Integration only, until omarchy #13690 rebases onto the ARM pacman
templates. pacman-templates-test still expects x86 refresh to copy
the templates directly.
Needs both the Chinese language and the region switch: the Chinese
labels for Region and China Apps, the region switch's logout prompt
through omarchy-text, and the language half of manual/52-china.md.
The installer asks for the language before the keyboard, and
Setup > Language (omarchy language en|zh) switches it later. Under
zh_CN the menu reads its labels from omarchy-menu.zh-cn.jsonc, the
shell from I18nZh.js through I18n.tr, and notifications are worded
by their sender through omarchy-text, which reads the same table.
User text is never translated.
* Revert "Merge pull request #11874 from acrogenesis/feature/usb-device-authorization"
This reverts commit 5cb31317b4, reversing
changes made to 9d48fc402e.
* Undo USB and Thunderbolt device approval on machines that ran it
* Verify boot rebuilds, recover Thunderbolt journals and prove USBGuard ownership in the removal migration
* Read the boot partition from ESP_PATH and stop on failed Bolt reads in the removal migration
* Recover Bolt's original mode from the setup journal in the removal migration
omarchy region cn|global switches an installed machine: cn upgrades,
trusts ArchLinuxCN through the rendered China config, records the
marker and links the developer mirrors, then refreshes pacman;
global undoes only what Omarchy made. The developer mirrors reach
new sessions only, so a terminal switch offers to log out.
Install > China Apps, shown only in China, installs WeChat, QQ,
Feishu, DingTalk, Tencent Meeting and WPS Office from the AUR and
sets up Tencent Docs, Yuque and Shimo as web apps.
manual/52-china.md describes the China region.
npm, PyPI and uv, Go, rustup and crates.io, RubyGems and Bundler,
and Maven reach their registries slowly or not at all from China.
On a cn install, link the profile's environment, cargo and gem
drop-ins into place. A regular file the admin wrote is never
replaced; the install fails instead.
The generic assigns swap sans-serif, serif and monospace for fonts
with no CJK glyphs, so tagged Chinese text ends up on whatever Noto
CJK variant the charset scan finds first, usually the Japanese one.
Fold the many Chinese tags down to one per variant and prepend the
matching family ahead of the assigns.
* Cache workspace layouts instead of running a script on every switch
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Test the workspace layout cache by running its logic, not matching its source
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep the fake layout read running while its exit handler runs
Addresses Codex review: clearing running first let an undeferred, broken retry pass the cache tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Track the power profile through Quickshell instead of polling busctl
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Recover the power profile when the daemon was down at shell start
Addresses Codex review: Quickshell never attaches its PowerProfiles listener if the daemon failed at startup. One-off reads on startup, power source changes and an IPC refresh from omarchy-powerprofiles-set cover that, still with no timer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Don't let a stale power profile read win over a change signal or drop a refresh
Addresses Codex re-review: a busctl read in flight could overwrite a newer PowerProfiles signal, and refreshes requested while a read ran were dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Test power profile transitions and stale reads as behavior, not source text
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Refresh the passwordless sudo indicator on change instead of polling
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Refresh the sudo indicator after failed changes and on a wall-clock deadline
Addresses Codex review: a rolled-back renewal skipped the refresh, the monotonic user timer paused through suspend, and default timer accuracy allowed a minute's delay.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Test the sudo indicator refreshes by running the grant flows
Replace source-string checks with runs of timed, renewed, failed, permanent and disable flows against the fixture, asserting the recorded omarchy-shell refresh and wall-clock systemd-run --user timer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A theme switch that plays an intro gave the shell the new still only when
the intro failed. The shell kept the old theme's path, which the theme swap
deletes, and loaded it again when OWE handed the desktop back, leaving it
empty. The new still is now handed over either way.
The shell also re-reads the background link whenever OWE hands the desktop
back or a wallpaper fails to load, falling back to the theme's first
background when the link itself is broken.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Hyprland lets any client take over a held lock, so a second shell started
beside the desktop judged the real shell's live lock stranded, took it, and
then exited, leaving the session behind the failsafe while the real shell
still reported itself secure. Recovery now waits while any other Quickshell
or hyprlock runs, and gives up if one never exits.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Pick window transparency from Appearance > Window Transparency
Transparent, the default, keeps today's windows. Opaque turns the windows
Omarchy makes transparent solid. Glassy is the frosted glass Starship
(#14726): its blur behind every see-through window and shell surface, and a
see-through, blurred background in the theme's terminal files, since foot
turns compositor blur down and a Hyprland opacity rule fades text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Leave window transparency to Appearance when animations are off
Turning animations off no longer makes windows opaque. VM installs start
with opaque windows instead, and a migration keeps them opaque for anyone
who already has animations off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add a Glassy bar background, under Menu Bar > Bar Background
Menu Bar > Bar Background picks Opaque Bar, Glassy Bar or Clear Bar, and
Indicators becomes Bar Indicators to match. Glassy keeps the theme
background faintly and Hyprland blurs what is behind it, with the same blur
as Glassy windows.
omarchy bar background opaque|glassy|none|cycle sets it and prints it when
given nothing. omarchy bar transparent true|false|toggle stays for
compatibility, as none and opaque. Double-clicking the bar cycles through
the three backgrounds the same way, from clear to glassy to opaque.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Stop when a saved corner choice can't be removed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Make btop follow glassy windows
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Signed, Omacom's PDF signing app, takes Xournal++'s place in the base
install and the preinstall set, so Remove and Install Preinstalls cover
it too. A migration installs it on existing machines, unless their owner
has removed the preinstalls; Xournal++ stays on machines that already
have it. The Filling out PDFs chapter now walks through Signed, the GUIs
chapter introduces it, the setup tip points to it, and the unused
Xournal++ config is gone.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Expect Appearance before Toggle under Trigger
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Read the default workspace layout once for all the menu rows that ask
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Pick window corners from Appearance > Window Corners
Theme Corners leaves them to each theme. Sharp and Rounded override every
theme through a Hyprland toggle, with Rounded using Solitude's corners.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep the corner choice when saving a new one fails, and test the load order
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Every preset the clock cycles through gets its own row, labeled with how
it reads, and the current one is checked. omarchy-bar gains get <id> <key>
and a bare position to read the settings the rows compare against.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Match the menu test to the rearranged Trigger menu
Trigger now leads with Toggle and Appearance, and Appearance's entries,
now including Herdr Theme Sync, are in their new order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Move the Menu Bar from Style to Appearance, with Hide and Show
Trigger > Appearance > Menu Bar now holds the bar's Position and
Transparency from Style > Menu Bar, and offers Hide while the bar shows
and Show while it is hidden, instead of a bare toggle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Name the Menu Bar entries after the bar, and split transparency
Hide Bar, Show Bar, Position Bar, Top Bar and the rest read on their own
when searched. Transparency becomes Transparent Bar while the bar is
opaque and Opaque Bar while it is transparent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Hovering the indicators revealed it dimmed alongside the ones that can be
switched on, though there is nothing to do with it there. An indicator can
now opt out of that reveal, and remote session does.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The locked binding once read true after an unlock, though the request,
the session lock, and its secure state were all false. A lock before
suspend then took the session for locked and did nothing, and the
machine slept unlocked. Lock requests, the status reply, and stranded
lock recovery now read those three values directly.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Let the workspace layout test's Lua assertions fail the suite
Lua 5.5's standalone interpreter exits 0 when the script that raised the error came in on stdin, and exits 1 for the same error in a script file. The block here was piped in through a heredoc, so every assertion in it reported as passing however wrong the value was: changing the expected layout to one the toggle never writes still left the file green.
* Add a floating mode to the workspace layout cycle
Super + L cycled a workspace between dwindle and scrolling. It now carries on into floating, where every window on the workspace floats.
Floating cannot be a third `layout` on the workspace rule. Hyprland has no layout by that name and accepts one anyway: `hl.workspace_rule({ layout = "floating" })` returns ok and silently leaves the workspace on dwindle, exactly as a misspelt name does. So the mode floats the windows themselves, through the same `float` dispatcher SUPER + T uses.
Ending the mode has to tile the windows the mode floated and only those, so each one is tagged with `omarchy-mode-floated`. A window already floating when the mode reaches it -- an app with a float rule of its own, a dialog, one popped out with SUPER + T -- is left unclaimed and is still floating when the mode ends. The tag lives on the window rather than in a table here: it dies with the window, follows it between workspaces, and survives the config reload that plugging in a monitor performs, none of which a table on this side would do. A pinned window is left alone entirely, because tiling one unpins it as well.
Windows arrive on a workspace three ways and a window rule only covers one of them, so the mode watches events: window.open for what opens there, window.move_to_workspace for what is carried in, and the same event for what is carried back out, which gives up the floating the mode gave it rather than staying floating on a workspace that tiles. Every tiled window is claimed before any of them is floated, because floating one member of a group floats the rest, and claiming as it went would find those already floating and leave them stranded.
The mode is read back from the saved file rather than from the compositor. Hyprland reports a workspace's tiled layout and floating is not one, so a floating workspace still answers dwindle or scrolling; trusting that answer would skip a step of the cycle. A file saved before floating existed names a layout rather than a mode and falls through to what Hyprland reports, so the cycle keeps working and the next press rewrites it.
Two faults in the old script had to go first, because saving the mode makes both persistent. Named workspaces report a negative id, which passed the `^-?[0-9]+$` guard, and Hyprland reads a leading "-" as a selector relative to the current workspace: pressing Super + L on a named workspace reconfigured workspace 1 instead, and would now save a file that did it again at every login. And the saved state was written under $HOME/.local/state while the Hyprland side reads XDG_STATE_HOME, which also had to reach package.path: the loader lists that directory but loads the files by module name, so a state home anywhere else listed the saved layouts and then loaded same-named ones from the home directory.
Saving is checked too. The mode reaching the compositor and the mode reaching disk are separate things that can fail separately, and a save that failed silently would leave the next press reading a file that never got written, cycling between two modes and never reaching the third.
The guard against named workspaces stops new files being written, but an earlier version already wrote them, and they go on being loaded at every login. A migration removes the saved layouts named for a negative id.
The keyword fallback is gone. `hyprctl keyword` refuses a Lua configuration outright -- "keyword can't work with non-legacy parsers. Use eval." -- and exits 0 while doing so, so as a fallback it neither worked nor reported that it had not. A failed apply now says so and saves nothing, rather than announcing a layout the screen does not show and restoring it at the next login.
Co-Authored-By: Codex XHigh <noreply@openai.com>
* Add titlebars, edge snapping, and a window Shelf to floating workspaces
Floating workspaces become a traditional desktop that works with every
theme. Windows get themed titlebars from the omarchy-hyprland-titlebars
plugin, which is loaded only while something floats. Dragging a titlebar
to the left or right edge snaps the window to that half, and dragging it
to the top or double-clicking maximizes it. The window floating mode
places is sized, centered, and cascaded, and floating workspaces focus
on click.
Super + M, or the minus button, sets a window aside on the Shelf, which
works from any workspace and restores the window onto the current one
in that workspace's layout. Float All Workspaces makes floating the
default while Super + L still sets a single workspace's layout.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Bring resize on border to tiling too
* Fix floating mode cleanup, fullscreen windows, and failed Shelf notes
Turning Float All Workspaces off with no workspace layouts left behind
started nothing to give the floated windows back. A window fullscreen
when its workspace began floating now floats once it leaves fullscreen.
A window whose Shelf note cannot be saved stays where it is, and the
titlebars read the theme from where omarchy-theme-set writes it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Place the Shelf on the bar once, instead of on every minimize
Putting it back on each minimize kept a Shelf the user moved where they
put it, but re-added one they had removed. A migration places it once for
existing bars; Super + Alt + M opens the Shelf either way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Raise titlebars off the window background
A titlebar in the theme's background color ran into the window under it.
It takes the theme's lighter background, the raised surface every theme
defines, and the selection color on themes that make the two the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drop minimizing and edge snapping from floating workspaces
A floating workspace is now titlebars with maximize and close, and nothing
else. The Shelf (its bar widget, panel, commands, notes, Super + M and
Super + Alt + M) goes with the minimize button, and the titlebar plugin's
edge snapping is left off, along with the shell's snap preview.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Stop describing the titlebar package as snapping
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Name web app windows in their launchers
Chromium names a web app window after its URL, and titles it with the page
title or just the domain. StartupWMClass ties the window to its launcher,
so the titlebar plugin can show the launcher's Name instead: WhatsApp
rather than web.whatsapp.com. New web apps, the shipped ones, and existing
launchers (through a migration) all carry it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drop the titlebar package comment from the base list
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Show the workspace layout in the bar and cycle it on click
The focused workspace's mark is a square for dwindle, a triangle for
scrolling, and a circle for floating, and clicking it cycles the layout
as Super + L does. Both layout toggles ask the bar to read the new mode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Show scrolling as a circle and floating as a triangle
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Move the look of the desktop to Trigger > Appearance
Menu Bar, Battery Percentage, Workspace Layout, Window Gaps, Animations,
and 1-Window Ratio change how the desktop looks rather than switching a
behavior, so they leave Trigger > Toggle for their own menu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Choose a default workspace layout instead of floating everything
Trigger > Appearance > Workspace Layout sets the layout of every workspace
without one of its own: Tiling, Scrolling, or Floating. A tiled default
is Hyprland's own layout, and a floating one is the floating mode. It
replaces Float All Workspaces, which also overrode and set aside the
layouts workspaces had been given with Super + L.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Title the workspace layout menu Default Workspace Layout
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Use the animation icon for Appearance > Animations
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Open clipboard history from Trigger
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Leave edge resizing to looknfeel on floating workspaces
looknfeel now resizes from borders everywhere, so floating workspaces
only need to switch pointer focus to click.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Convert saved workspace layout rules to modes
Released versions saved a workspace's layout as a plain workspace rule,
which names no mode, so a floating default would override a workspace
set to scrolling, and the bar and Super + L would misread it. A migration
rewrites those files as modes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Cycle workspace layouts backwards with Super + Alt + L
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Test the Trigger > Appearance split and Clipboard History placement
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Decode escaped web app URLs before naming their windows
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Describe the workspace widget's layout mark and click
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Let a workspace cycled onto the default follow it again
Once Super + L gave a workspace a layout of its own, it kept it for good,
so a later change of the default never reached it. Cycling onto the
default layout now forgets the workspace's own instead of saving it.
Without a saved default, the default is Hyprland's own layout, which
the user's looknfeel may have set.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Move the look of the desktop to Trigger > Appearance
Menu Bar, Battery Percentage, Workspace Layout, Window Gaps, Animations,
and 1-Window Ratio change how the desktop looks rather than switching a
behavior, so they leave Trigger > Toggle for their own menu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Use the animation icon for Appearance > Animations
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Open clipboard history from Trigger
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Test the Trigger > Appearance split and Clipboard History placement
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Switch out of an input method and back with a lone Shift
Fcitx's alternate trigger only acts while an engine like Pinyin is or was
active, so Shift + letter and plain Latin typing are unaffected. It needs
Shift releases intact, which the both-Shifts Caps Lock option prevented.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Follow XDG_CONFIG_HOME in the Shift migration and test it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Remove skill body trigger conditions; bolster frontmatter content.
* Keep window behavior and screen recording in the skill's triggers
The removed "When This Skill MUST Be Used" section listed both, and the description that replaces it had dropped them, so a request to record the screen or change how windows behave no longer matched anything the agent reads before choosing the skill.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex Medium <noreply@openai.com>
---------
Co-authored-by: omarchybot <omarchybot@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Codex Medium <noreply@openai.com>
Co-authored-by: Omarchybot <317366263+omarchybot@users.noreply.github.com>
Nothing in the base install builds with clang: Qt apps and Ruby's native
gems compile with gcc, and Ruby comes prebuilt from mise. Rust crates that
use bindgen, including rb-sys gems, need libclang, so the Rust dev
environment installs it. llvm was unused even there.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Build the calendar month grid in UTC
The grid walked a cursor at local midnight and stepped it with setDate(getDate() + 1). Chile starts DST at 24:00 on the first Saturday of September, so local midnight of 6 September 2026 does not exist, and Qt's V4 engine resolves a missing local time backward -- onto 5 September 23:00, the same calendar date, which the next iteration reads again. September 2026 draws 30 31 1 2 3 4 5 and then 5 6 7 8 9 10 11, and every later date sits one weekday early. Cuba, Lebanon, the Azores and Egypt change over at 00:00 too and break the same way in their own transition month; where the gap lands on a leading padding cell instead, the whole six-week grid shifts by a day with no duplicate in it.
Node resolves the identical gap forward, so the suite has never seen this: running the shipped monthGrid under Node across all 598 tzdata zones for 2020-2035 reports nothing, while the same code under the QML engine breaks 402 grids. A sequence of civil dates never needed a wall clock, so it walks UTC, where every day is 24 hours long, and the test pins that by building the grid with a Date whose local constructor and accessors throw.
stepMonth reads a month back off the same kind of date. No zone puts a midnight gap on a first of the month, so it is not broken today, but it moves with the grid rather than waiting for a tzdata release to make it a second bug.
Co-Authored-By: Codex XHigh <noreply@openai.com>
* Name the calendar heading's month from local noon
The heading built a local midnight on the first of the month, and that midnight does not always exist: Cuba started DST at 00:00 on 1 April 2012, so in Havana the QML engine resolves it back onto 31 March and the heading reads March 2012 above an April grid. Noon exists on every first of the month there has been, and keeping it local is what makes Qt.formatDate print the right month in every zone.
The Apia case comes from #13186: Samoa skipped 30 December 2011 entirely, so unlike a one-hour midnight gap, Node's V8 cannot resolve its way past it, and the test fails on the old grid under Node instead of only in the shell.
Co-Authored-By: Codex Medium <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Codex XHigh <noreply@openai.com>
Co-authored-by: Omarchybot <317366263+omarchybot@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Monologue records and Omacut trims, which covers what most people used
them for. Existing installs keep both; there is no migration, and the
preinstall removal no longer treats them as Omarchy's to uninstall.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Correct omarchy-pkg-add AUR claim in AGENTS.md
* Say omarchy-pkg-add installs from the pacman repos, not the official ones
It runs plain pacman -S, which also reaches the [omarchy] repo every shipped pacman.conf enables, so "official repos" would send an agent to the AUR helper for Omarchy's own packages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex Medium <noreply@openai.com>
---------
Co-authored-by: Omarchybot <317366263+omarchybot@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Codex Medium <noreply@openai.com>
on hides the bar and off shows it, matching the literal flag name
rather than an inferred "bar visibility on" reading. The behavior is
correct and depended on by omarchy-toggle-fullscreen-desktop; this
just makes the --help summary say so.
Claude-Session: https://claude.ai/code/session_01YQK8wd7WU8qEVnXVnNmcSW
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* docs(agents): add plugin UI safety baseline
Document safe rendering and validation of dynamic plugin data after a marketplace review surfaced a QML AutoText boundary.
Assisted-by: Codex/GPT-5
* docs(agents): allow consented background refreshes
Clarify that plugin networking may refresh periodically when disclosed and user-controlled.
Assisted-by: Codex/GPT-5
* docs(agents): keep plugin guide label concise
Leave the safety baseline discoverable within the plugin guide without expanding its topic index label.
Assisted-by: Codex/GPT-5
* docs(agents): point plugin authors at their own markup helper
Omarchy ships no markup-neutralizing helper, and this skill is for installed systems, where the parent SKILL.md forbids editing the packaged shell components the old line suggested extending.
Co-Authored-By: Codex Medium <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Omarchybot <317366263+omarchybot@users.noreply.github.com>
Co-authored-by: Codex Medium <noreply@openai.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Omarchy never sets XCURSOR_THEME, so asking GSettings on every cold boot replaced the "default" alias with whatever GSettings held, which is the schema default Adwaita when nothing was ever set. That overrode a theme chosen through ~/.icons/default and exported the substitute to every application. The saved theme and GSettings are now consulted only when recovering from the blank startup theme or an interrupted reveal.
Co-Authored-By: Codex XHigh <codex@openai.com>
* Return Caps Lock to normal and make Shift + Caps Lock the compose key
Caps Lock as compose, with both Shifts as Caps Lock, confused too many
people. Stock XKB has no option for compose on the shifted Caps key, so
Omarchy defines one in /etc/xkb, which libxkbcommon reads before the
system rules. The explicit actions keep the Shift level from toggling
Caps Lock. Lone Shift is no longer part of Caps Lock, which leaves it
free for input methods.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Require xkbcli so the keymap check cannot silently skip
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Unify input methods with the installer keyboard selection
* Preserve inherited input keys and later keyboard changes
* Exercise input engines across desktop toolkits
* Show input modes in the keyboard indicator
* Keep Pinyin setup prompts visible
* Remember input selections before an app gains focus
* Remove the Japanese input welcome notification
* Show explicit choices on notification cards
* Suppress the Cloudpinyin first-use prompt
* Show the input switching shortcut in keyboard hints
* Verify input setup and composition stay quiet
* Use Super+I for input switching independently of Fcitx keys
* Restore linked input settings and attribute test notifications to Fcitx
* Correct the input setup shortcut hint
* Keep input tooltips to the name and switching shortcut
* Place the input language selector with the right bar widgets
* Show input switching only for multiple layouts or methods
* Unify input method and keyboard layout switching
* Select keyboard layouts and input methods in the Typing menu
* Apply typing selections immediately from the menu
* Use installer keyboard choices and keep typing selections accurate
* Queue typing changes and wait for saved selections
* Keep queued typing selections when reopening the menu
* Cycle keyboard layouts alongside composition engines
* Avoid shadowing Python typing and retain pending mixed input selections
* Keep input clicks working through controller and compositor failures
* Move the keyboard layout widget to the right side of existing bars
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Map keyboard choices to the layouts the installer sets
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep the switching order when changing input methods
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Set input method fonts only after the selection saves
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Restore confirmed picker checks when an immediate change fails
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Route Super+I through the shell's input reader so unfocused choices stick
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Restore the input group when the font preference cannot be written
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drop the azerty conversion now that the installer no longer offers it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Apply the owner's keyboard layout once before persisting the input choice
A quattro merge kept a second apply_keyboard after the retry loop, which
re-ran loadkeys and firstboot, ignored their failure, and overwrote the
XKB layout setup supplies for keymaps kbd-model-map lacks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Give the Lao keyboard choice a Lao desktop layout
The row used la-latin1, kbd's Latin American Spanish keymap, so Lao
installs typed Spanish. There is no Lao console keymap; like Korean, the
console stays US and the desktop gets la, which Omarchy keeps behind US
as a non-Latin layout.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Load engines installed after Fcitx started when they are selected
The Typing menu refused an engine whose package an update had installed,
since Fcitx discovers engines only at startup. Restart input once and wait
for the engine; name the package when it really is not installed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Cycle input methods back with Super+Shift+I
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drop notification action buttons now that the Pinyin prompt is suppressed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Cycle input only through the shell's reader
Without the shell there is no bar or menu either, so the one-shot cycling
path only duplicated the reader's logic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add engines from the command line through the Typing menu's path
omarchy setup input <engine> had its own restart, append, select and
live-rollback code in configure.py, parallel to the menu's. It now
installs the packages and appends through typing_setup, leaving
configure.py with install-time seeding and update defaults.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep Ctrl+Space out of Fcitx's toggles instead of probing for it
The terminal prefix bind read the Fcitx config and profile on every press
to avoid fighting a Ctrl+Space toggle. configure --defaults now removes
that key from every trigger list, so the bind only checks for a terminal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Run the input-method scripts with the system Python
A bare python resolves through PATH, so a mise, pyenv or venv interpreter
could run them without the system's PyGObject.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Hyprland discards Lua globals and timers during reload, so the pending startup reveal must survive outside Lua state. Preserve the normal cursor theme for this compositor session, recover orphaned blank settings, and repair activation environments before future applications inherit them.
🤖 Generated by GPT-6.1-Sol High in Codex through T3 Code. Reviewed by GPT-6.1-Sol XHigh in Codex through T3 Code.
Co-Authored-By: GPT-6.1-Sol XHigh <codex@openai.com>
kbd's azerty keymap is French AZERTY and there is no Azerbaijani console
keymap. XKB's az moves letter keys, so pairing it with a US console would
type passwords differently at disk unlock than on the desktop.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Both Thunderbolt loops reconciled or rescanned every 2 s even on machines with
no controller: on an idle M2 Max that was about 1,590 process launches a
minute. While nothing is connected or boot-protected and no controller is
present, the daemon now reconciles every 10 s, inside a snapshot's 20 s
lifetime, and returns to every tick as soon as a controller, a policy change
or a boot recovery checkpoint appears. The watcher skips an unchanged, fresh,
device-free snapshot, and both loops wait without forking sleep.
The rejection inside the new PID namespace ran without privileged mode, so
it never reached the caller lookup, and nothing would have caught a check
that accepted -p anywhere in the arguments.
The check read /proc/$$/cmdline. arch-chroot runs its command in a new PID
namespace beneath the outer /proc, where $$ names no process or someone
else's, so every bash -p entrypoint was rejected there with status 126.
The installer met this as soon as one was called at install time:
install/config/thunderbolt-authorization.sh runs
omarchy-thunderbolt-authorization-admin, and the edge ISO stopped in
"Configuring system" with "/proc/219/cmdline: No such file or directory".
The check runs in a child of the caller, so read the parent from that
child's /proc/self/stat, which is given in the numbering of the /proc it is
read through. Outside a PID namespace that is $$, as before.
Naming a unit's source in default/ counted as installing it, so a command
that never copied the unit, or copied it after enabling it, passed. Look
for the install or cp of that source ahead of the enable instead.
The first-run test accepted any script that tried three of the units. It
now requires one enable --now per unit and, with each unit unknown in
turn, exactly the calls of a clean run minus that unit.
The Thunderbolt approval watcher is enabled by its setup command, a
migration and first-run, but no package put it in a user unit folder and
nothing installed it. On edge the migration failed with "Unit
omarchy-thunderbolt-authorization.service does not exist", so every later
migration stayed pending. First-run failed too, and enabled none of its
units: given a list, systemctl enables nothing when one unit is unknown.
The units are now shipped by the settings packages (omarchy-pkgs#922).
Here, first-run enables its units one at a time, so a unit missing from a
build costs only that unit, and config-test fails when a unit under
default/systemd/user is enabled without being shipped or installed first.
* Add omarchy-hw-platform to tell Apple Silicon and Qualcomm machines apart
aarch64 covers Apple Silicon Macs, Snapdragon laptops, Raspberry Pis and
VMs, so the CPU architecture can't pick platform setup. omarchy-hw-platform
prints apple-silicon, qualcomm, generic-aarch64 or generic from the vendor
prefixes in the device tree's root compatible (/proc/device-tree, then
/sys/firmware/devicetree/base) and the CPU, and fails when they contradict
each other. Its qualcomm answer uses the same evidence as the dragon
branch's omarchy-hw-qualcomm-soc, and the fixtures check the two agree.
omarchy-hw-apple-silicon is the Apple predicate on top of it.
An image built away from its hardware names its target in a root-owned
manifest, /var/lib/omarchy/image/target (format=1, platform=). While the
root is being built rather than booted, the detector answers from it and
never reads the build host's device tree; a booted system always answers
from its hardware. As root it restarts in an empty environment, so no
variable picks the platform for privileged setup.
Co-authored-by: Birk Skyum <74932975+birkskyum@users.noreply.github.com>
Co-authored-by: Jimmy Van Veen <8206925+JimmayVV@users.noreply.github.com>
* Reject platform gates keyed only on the CPU architecture
An architecture check is right for an ABI, a binary or a repository's
$arch, not for picking a platform. The lint lists each shipped check after
review and fails on a new one, so platform setup goes through
omarchy-hw-platform.
* Compose default packages from base, architecture and platform lists
omarchy-reinstall-pkgs installed the base list on every machine. A
platform's default set is now the base list, then install/omarchy-aarch64.packages
on aarch64, then the platform's own list when it has one
(install/omarchy-qualcomm.packages adds linux-firmware-qcom).
omarchy-pkg-defaults prints the set for the detected platform, or for the
one named, and reinstall installs it. x86_64 still gets exactly the base
list, and an image build gets its target's set.
* Early-load thunderbolt only where the kernel builds the module
linux-aarch64 has no thunderbolt module, and mkinitcpio fails the whole
image over a MODULES entry it cannot find. The optional form, thunderbolt?,
is not enough: a systemd initramfs copies MODULES as written into
modules-load.d, where "thunderbolt?" names no module, and a kernel without
the module would still list it there. thunderbolt_module.conf now adds plain
thunderbolt only when modinfo finds it as a module for the kernel being
built.
Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>
* Compose mkinitcpio HOOKS from an early baseline drop-in
omarchy_hooks.conf assigned HOOKS outright, and mkinitcpio sources drop-ins in
name order, so it ran after any numbered drop-in (90-*.conf) and threw its
hooks away. HOOKS now comes from 00-omarchy-hooks.conf, which sorts first, and
later drop-ins add to it. The NVIDIA kms filter and the vconsole handling stay
in omarchy_hooks.conf. With the drop-ins Omarchy writes, machines keep the
same HOOKS, MODULES and FILES; a user drop-in that sets HOOKS and sorts before
omarchy_hooks.conf now takes effect instead of being overwritten.
Migration 1786605598 now sources the baseline first, since without it the
migration would read empty HOOKS and also rebuild hybrid systems. It skips a
machine that lacks the baseline.
* Resolve the LUKS parent from raw lsblk output
lsblk -s prints the ancestor tree with box-drawing prefixes ("└─/dev/nvme0n1p6"), so the crypto_LUKS parent came back as a path that does not exist and first-boot setup stopped with "Could not locate the LUKS device to re-key". -r prints raw names. Factory reset uses the same lookup.
* Check the factory-reset passphrase against keyslots only
cryptsetup open tries enrolled tokens before the key file, so a TPM2 or
FIDO2 token could authorize the re-key with a wrong passphrase.
* Test LUKS parent lookup during owner setup and factory reset
Exercise raw ancestry output without invoking provisioning or reset. Cover btrfs subvolume roots, absent encryption, failed probes, and explicit cryptdevice specifications.
* Cover LUKS parent lookup through an LVM volume group
* Journal the first-boot LUKS re-key so an interrupted attempt resumes
A first boot killed after the staged install key's slot was retired left the
key file behind, and every retry then stopped at "the staged LUKS key no
longer unlocks". A lost key file with the boot-time auto-unlock still
configured let setup finish with the disk unlocking unattended.
The re-key now runs from install/provisioning/luks-rekey.sh and records its
phase and slot numbers (never key material) in
/var/lib/omarchy/provisioning/luks-rekey.state. It reuses the owner's slot
instead of adding a duplicate, retires LUKS1 slots too, and destroys the
staged key only after the volume proves it opens nothing. Setup refuses to
finish while any part of the provisioning unlock remains. The journal ends at
phase=done and leaves together with pending, so a retry after the staged key
is retired must use the password the disk already holds.
Key checks exclude LUKS2 tokens: cryptsetup open tries an enrolled token
(say a TPM2 slot a previous owner left behind) before the key and reports its
slot whatever key it was given. Slot listings read only the keyslot section,
since a luks2-keyring token looks like a keyslot in luksDump.
* Change the root volume's LUKS key before the login and root passwords
Installation gives the login user, root and the disk holding / one password,
but omarchy-drive-password changed only the LUKS key, so the three drifted
apart. For the disk holding / it now checks the current password opens the
disk, changes the LUKS key, confirms the new key opens the disk and the old
one no longer does, and only then sets the login and root passwords. Any other
encrypted drive changes its LUKS key alone. Every drive now asks for the
current password with gum instead of cryptsetup's prompt.
A journal in ~/.local/state/omarchy/drive-password.state (disk UUID, key slots
and phase, never a password, written with the re-key journal helpers) lets the
next run finish an interrupted change. The rerun accepts only the old key
(removing the slot the interrupted change added) or the new one (retiring the
old slot), so another key such as a recovery passphrase never becomes the login
password. luksChangeKey is judged by whether the new key opens the disk, not
by its exit status, and key checks exclude LUKS2 tokens, which cryptsetup
open would otherwise accept in place of any password.
Runs are serialised, cryptsetup calls hold a disk lock, a / whose LUKS
partition cannot be resolved fails closed, and passwords are never traced. A
new password equal to the current one is refused, and --pbkdf argon2id is
passed only to LUKS2, so LUKS1 drives can change their password again.
* Add omarchy-lifecycle-dispatch for platform boot operations
Provisioning, factory reset and update call a fixed set of operations
through it. A platform whose boot chain the generic Limine path can't drive
registers a directory of root-owned entrypoints in the dispatcher; every
other platform gets a no-op and keeps its generic path. Apple Silicon is
registered to omarchy-mac-boot's /usr/lib/omarchy/mac-boot, with its
required operations failing clearly when the package or an entrypoint is
missing. docs/lifecycle-dispatch.md is the contract, including how a
Qualcomm boot package would plug in.
* Let the platform's boot package own first-boot setup's boot chain and key slots
Owner provisioning asks provision-prepare before the owner form, hands the
staged unlock to provision-commit and provision-verify where the platform
implements both, and records the kept LUKS slot through luks-slots before
the staged key is destroyed; the disk password change records the owner's
new slot the same way. On x86, Snapdragon and generic aarch64 every call is
a no-op and first boot is unchanged.
* Reset through the platform's boot package where it owns the boot chain
Where a platform implements the reset operations, reset-prepare and
reset-verify stage and prove the factory root's boot files, the throwaway
slot is added only after that, and reset-commit gets the key on standard
input once the factory root is active. A failure before the switch rolls the
boot state back and revokes the slot. Elsewhere the Limine UKI path is
unchanged.
* Check the platform's boot chain after an update
omarchy-update-boot runs update-verify after AUR packages, the last
sudo-capable step, so it also covers the initramfs rebuilds they trigger.
It follows third-party build code, so like AUR it authenticates through the
no-update wrapper. A failed verification finishes the update's remaining
steps, then exits non-zero without offering the reboot. Where the platform
does not implement it nothing runs and nothing asks for root.
* Hand a machine to its platform's migration through lifecycle dispatch
migrations/1790347292.sh resolves the new optional migrate operation and,
where the platform's boot package implements it, runs it as root. The
entrypoint moves the machine onto the platform's official package set,
idempotently and resumably; its failure leaves the migration pending. Once
it succeeds, a root-owned marker spares other accounts the migration and its
root prompt. Everywhere else, Macs without the boot package included, the
migration is a no-op, and an undetermined platform keeps it pending.
* Refuse packages tagged for another platform in a pacman hook
A platform package names its platform with groups=('omarchy-platform-<name>'),
<name> being what omarchy-hw-platform prints. omarchy-settings ships a
pre-transaction hook, the guard it runs and a copy of the detector, so the
guard is resident before the runtime and any platform package. The guard
reads tags from the synced databases and refuses a transaction that installs
a package tagged for another platform; a transaction without a tagged
package passes without asking which machine this is. Image builds answer
from the image's root-owned target manifest, as the detector does.
The guard ships on aarch64 only: no platform package is built for x86_64,
so x86 transactions run no hook. Hardware setup starts by proving the guard
is resident and checking the packages an installer placed without it. On
generic aarch64, where no platform packages exist, a missing guard is
logged, not fatal, and on x86 it is expected.
docs/platform-guard.md is the contract.
Co-authored-by: Birk Skyum <74932975+birkskyum@users.noreply.github.com>
Co-authored-by: Jimmy Van Veen <8206925+JimmayVV@users.noreply.github.com>
* Enable Voxtype's Vulkan backend only when it is installed
omarchy-hw-vulkan reports a Vulkan runtime, not whether this voxtype build
has a Vulkan backend. The aarch64 voxtype-bin package ships only the CPU and
ONNX binaries, so on any aarch64 machine with a Vulkan driver setup ran
`voxtype setup gpu --enable` and printed "Vulkan backend not installed" in
the middle of an otherwise working install. Require the backend binary too.
x86_64 voxtype-bin always ships /usr/lib/voxtype/voxtype-vulkan, so x86
installs still switch to the GPU backend as before.
Ported from omacom/omarchy-mac#483.
Co-authored-by: Oliver Lukschander <33756270+oliverlukschander@users.noreply.github.com>
* Link the system Widevine CDM into Brave
The widevine package registers its CDM for Chromium and Firefox only.
Brave looks for the CDM in its own install directory, finds nothing, and
DRM sites (Netflix, Apple TV+, Spotify) fail with a blank player and no
useful error. This hits machines where Widevine comes from that package,
such as aarch64 ones, since no Widevine component is published for them.
Link /opt/WidevineCdm/chromium into /opt/brave-bin and /opt/brave-origin-bin
through as_root when Brave or Brave Origin is installed, and add a migration
that repairs existing installs. Both do nothing when the widevine package is
absent or the browser already has a CDM. A standard x86 install has no
widevine package, so nothing changes there.
Ported from omacom/omarchy-mac#474, extended to Brave Origin.
Co-authored-by: Caleb Marshall <68087341+cajomar@users.noreply.github.com>
* Report failed setup steps on the console
With file logging, run_logged sends a failing leaf's output and its
"Failed:" marker to the install log only, so the caller sees setup stop
with an exit status and nothing else. A missing command (exit 127) was the
case that made this hard to diagnose.
Print the failed script, its exit status and the log path on stderr. Stdout
logging mode already shows the failure marker and is unchanged, and
successful steps stay quiet.
Ported from omacom/omarchy-mac#389 (the logging part).
Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>
* Let a migration defer itself without stopping the update
A migration that exits 75 changed nothing and cannot finish yet: omarchy-migrate
leaves it pending with its login notification, runs the migrations after it and
exits 0, so omarchy update carries on. Any other failure still stops the queue.
The platform migration passes a deferred refusal of its entrypoint through, and
defers itself where the dispatcher can't resolve its entrypoint (the platform
can't be told, or the entrypoint isn't trusted): nothing has run then, so the
update goes on to its boot verification, which fails there, as documented.
* Keep Apple Silicon's systemd initramfs on the HOOKS baseline
00-omarchy-hooks.conf set the busybox line on every machine. On a Mac,
omarchy-mac-boot's drop-ins then add the Asahi firmware hooks, but its
encryption drop-in reads busybox encrypt as a Mac unlocked through
cryptdevice= and leaves the line alone. The image came out as
"... block encrypt asahi omarchy-vendorfw filesystems ...", with no
systemd, sd-encrypt or omarchy-mac-encrypt, so an encrypted Mac could not
unlock its disk. Reproduced against an M1 Pro's installed drop-ins.
The baseline now asks omarchy-hw-platform. Apple Silicon starts from the
systemd line its boot package builds on. A Mac whose own mkinitcpio.conf
carries the asahi hook, or busybox encrypt, keeps that line, as a legacy
install needs. x86, Snapdragon and generic aarch64 keep today's busybox
line, as does a machine with no detector. The detector is the runtime's,
or else the copy omarchy-settings ships for its platform guard, so a
settings package newer than the runtime still places a Mac. A detector
that cannot place the machine stops the build.
The HOOKS test now composes the baseline on each platform and on legacy
Mac lines. A new test composes it with omarchy-mac-boot 20260926-1.45's
drop-ins, kept as fixtures, and checks the exact HOOKS the M1 Pro builds.
The NVIDIA test pins its platform.
* Hide optional installs that have no package on this machine
Not every optional install exists on every architecture: some vendors ship no
aarch64 build, and some repositories don't carry a package at all. Each
Install row now carries a `when:` guard naming every package its install
command adds, checked with omarchy-pkg-available, so a row with nothing to
install disappears instead of failing halfway through a pacman transaction.
The rows that build from the AUR (Chrome, Edge, Brave, Brave Origin, Zen,
NordVPN) are guarded on the CPU architectures their vendors publish for.
omarchy-pkg-available asks the sync databases: one `pacman -Slq` per batch,
and `pacman -Sp` for a name that list doesn't hold, so a provided name
(libappindicator-gtk3) and a version constraint resolve the way `pacman -S`
will. The menu's guard prelude sources the same function, so the whole batch
still costs one fork. omarchy-reinstall-pkgs reports and skips a default
that no configured repository offers instead of failing the transaction.
On x86_64 every guarded row resolves against the stable and edge
repositories, so the menu is unchanged there.
Tests derive each row's packages from its install command on x86_64 and
aarch64 and fail when the guard names different ones.
Co-authored-by: Ryan Hughes <1630358+ryanrhughes@users.noreply.github.com>
* Run 1Password with software GL where there is no render GPU
Electron draws a blank window when the machine has no DRM render node: a VM,
a headless or passthrough GPU, or a GPU whose driver isn't up yet (Apple M3
and later on Linux today). omarchy-hw-render-gpu tells whether a renderD node
exists, and omarchy-cmd-electron-gl-args prints --ozone-platform=wayland
--disable-gpu only when it doesn't.
omarchy-cmd-electron-gl-wrap installs a marked PATH wrapper in /usr/local/bin
that execs the real binary with those flags, so it follows the machine rather
than the moment it was installed. It refuses to replace a launcher it didn't
write, or to wrap a binary over itself, and stages the write so a failure
leaves the old launcher in place. omarchy-cmd-desktop-exec-repair points the
user's desktop entry at the wrapper by replacing only the executable token,
keeping arguments, other actions and customizations, with a backup.
The 1Password installer uses them when the machine has no render node. With
one, which is every x86 machine with a working GPU, nothing changes.
Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>
* Record with wf-recorder when gpu-screen-recorder can't start
gpu-screen-recorder knows NVIDIA, AMD, Intel and Broadcom GPUs and aborts
during GL init on anything else, such as Apple Silicon's, before a recording
starts. Screen recording now falls back to wf-recorder, when it is installed,
if gpu-screen-recorder is missing or exits before the file appears. The
picker, file naming, notifications and post-processing are shared. wf-recorder
records one audio device, so desktop plus microphone go through a temporary
null sink that stop unloads, and its track is pinned to 48 kHz. Portal
capture and --resolution stay gpu-screen-recorder only.
The finalize pass keeps 48 kHz too: loudnorm declares 192 kHz output, so
without -ar the AAC encoder wrote 96 kHz from a 48 kHz recording, which many
editors decode as silence, and which on Apple Silicon makes the speaker
safety daemon lock the amps. #11092 makes the same one-line change; whichever
lands second drops it.
Stop and status matched `pgrep -f ^gpu-screen-recorder`, which misses
wf-recorder and matches any command line that starts with that path. The
script now records the recorder's pid, and the new
omarchy-capture-screenrecording-process checks or signals it by its
executable, through a pidfd opened before the check so a reused pid can't be
signalled. When that pid is no longer a recorder (a recording started before
this change, or one that died), every recorder the user runs counts, as it
does for the bar indicator and the Stop row, which use the same helper.
Where gpu-screen-recorder works, which is every x86 GPU it supports today, it
records as before.
Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>
Co-authored-by: Issac Trotts <15530+ijt@users.noreply.github.com>
* Hold logind's lid switch while a display it doesn't count is in use
systemd-logind treats a closed lid as docked only when an external display
is on a connector type it lists (HDMI, DisplayPort, DVI, VGA...). Displays on
"USB" connectors, which Apple Silicon uses for USB-C displays, aren't on that
list, and a missing comma fuses DVI-A and Composite into one entry. With such
a display in use, closing the lid suspended a laptop that was still being
used on it.
omarchy-system-lid-inhibit holds a handle-lid-switch inhibitor while one of
those connectors is connected and enabled in Hyprland, on a laptop where
HandleLidSwitchDocked is ignore, which is exactly the case logind would have
ignored had it counted the display. It releases the inhibitor when the display
goes. A compositor or logind that doesn't answer changes nothing, since
releasing then would suspend a closed lid. The inhibitor runs as a transient
user unit bound to the monitor watcher's unit, so a watcher that dies can't
leave the lid inhibited until logout.
The monitor watcher runs it alongside the clamshell sync. On a machine whose
external displays are all on connectors logind counts, it does nothing.
* Keep the focused workspace in front when a monitor goes away
When a monitor is unplugged, or the lid turns off the laptop panel, Hyprland
first warps focus to whichever monitor it lists first and only then moves the
removed monitor's workspaces over. The workspace you were on lands hidden
behind that monitor's own (Hyprland 0.56.2, CMonitor::onDisconnect).
monitor.focused fires before Hyprland records the new focus, so the monitor
still reported as active is the one being left: its workspace is remembered
until the event loop next turns, and a monitor.removed in that window focuses
it again once the removal has finished. An open special workspace already
comes along, and any focus or workspace change made before the restore runs
wins.
* Drive the keyboard backlight from the ambient light sensor
Laptops with an ambient light sensor and a keyboard backlight LED now light
the keys in the dark and dim them as the room brightens, inverse to display
auto-brightness. omarchy-brightness-keyboard-auto polls the sensor every five
seconds (preferring an IIO device that names itself ALS, and converting a raw
channel with its offset and scale) and maps lux onto the LED, with a deadband
so it doesn't chase noise. It pauses while the screen is locked or the lid is
closed.
A level set with the keyboard-brightness keys wins until the light changes
enough that it no longer fits. omarchy-brightness-keyboard records each level
it sets in the session's runtime directory (a private state directory without
one), so the loop can tell keys turned off on purpose from keys left off by
the lock screen or a restored zero, which it lights again.
The user unit runs it in the graphical session. Its ExecCondition asks
--available, so it is a no-op on machines without both a sensor and a
keyboard LED. First run enables it, and a migration enables it for existing
installs, writing the enable symlink itself when there is no live user
manager.
Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>
Co-authored-by: Gabe Ohlson <3355909+6abe@users.noreply.github.com>
* Defer an image build's hardware setup to the machine's first boot
An image is built away from the machine it will run on, so its hardware
leaves would probe the builder. While the image manifest the detector
already reads (/var/lib/omarchy/image/target) exists, omarchy-apply-hardware
queues every leaf of install/hardware/all.sh instead of running it and arms
omarchy-provision-hardware.service. On the first boot, before owner setup
and the login screen, omarchy-provision-hardware retires the manifest and
runs the queue on the real hardware: a failed step stays queued for the
next boot (exit 75), and the initramfs is rebuilt once after the last step
when a step changed its inputs or asked for it. Bluetooth starts its
service on that boot, whose target is already up. Root ignores any
environment override of these paths. Without a manifest, as on every
ISO install, hardware setup runs exactly as before. Nothing here fetches
package databases: an image carries its sync databases and every package
its steps install.
* Give an image-built root a UTF-8 locale when LANG is still C
A root built from a distribution tarball instead of the ISO (Arch Linux
ARM's ships LANG=C) never went through the ISO's locale step, so it ran
non-UTF-8. The config phase now selects en_US.UTF-8, generating it if
needed, but only when LANG is unset, C or POSIX: any named locale is
somebody's choice and stays. The ISO writes the locale before this runs,
so an ISO install is unchanged.
Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>
* Let a platform's packages run their own boot, system and user setup
Three dispatch operations give a platform one place for the setup it
needs beyond Omarchy's, instead of inline platform branches in the setup
leaves. The last hardware leaf runs setup-boot, then setup-system, as
root, so an image build queues them with the others and its first boot
runs them, with image-first-boot (offline-safe, asking for the one boot
rebuild instead of building). setup-boot belongs to the boot package and
is required on Apple Silicon, where hardware setup has no other way to
run the Mac's boot setup; a failed setup-boot stops the leaf before
setup-system. setup-user runs as the user being set up, refuses root,
and gets only the user's home and session variables: from the last user
leaf at finalization (skipped inside an image build, where the detector
would read the build host) and again from first run once the session is
up. On Apple Silicon setup-boot resolves in omarchy-mac-boot's directory
and the other two, both optional, in omarchy-mac's, /usr/lib/omarchy/mac;
x86, generic aarch64 and Qualcomm register none, so all three are no-ops
there.
A failed finalization now keeps first run pending, like any failed
first-run step, instead of letting the other steps mark it complete.
Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>
* Find batteries, lids and panels that aren't named like a PC's
Laptop detection assumed PC names: a battery called BAT*, a lid in /proc/acpi, a
touchpad or trackpad in the device name, a GPU backlight. Apple Silicon has
none of them (macsmc-battery, the SMC lid switch, apple-mtp-multi-touch,
apple-panel-bl), and some Framework and Chromebook firmware calls its battery
CMB0, so those machines lost the battery panel, laptop-only services, lid
handling, the touchpad toggle and display brightness.
- Battery presence goes by type instead of name, and skips a battery scoped
Device (a wireless mouse or keyboard), so a desktop with one isn't a laptop.
- Battery status takes the first UPower battery that powers the machine, reads
thresholds and cycles from it (BAT* stays the fallback), shows the magnitude
of a signed power_now, and rounds the percentage half-up for display like the
bar, while the charge-hold check keeps the unrounded value.
- A laptop is also one with any input device that reports a lid switch
(SW_LID), whatever its name: Apple's SMC input device has the same name on a
Mac mini, where it reports none. Whether the lid is closed comes from ACPI
where there is an ACPI lid, as before; otherwise logind is asked, with a
short timeout, only when a device reports a lid switch.
- A device named touchpad or trackpad still wins; only when there is none does
the touchpad match take a mouse udev marks as a touchpad (Apple's
multi-touch trackpad), never a touchscreen.
- The platform package can describe its displays in
/usr/share/omarchy-platform/displays.conf: backlights that never drive the
panel (a Touch Bar's), the panel's own where there is no GPU backlight, and
a display driver without DDC channels, where a probe walks unrelated I2C
buses for about 3 s: there only a connector with a ddc node is probed, and
a monitor without one reports no brightness instead of dimming the built-in
panel. Omarchy ships no such file and names no device in it; without it the
backlight choice and DDC probing are unchanged.
Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>
Co-authored-by: Ryan Hughes <1630358+ryanrhughes@users.noreply.github.com>
Co-authored-by: Sparsh Kumar <22640187+skkumarsparsh@users.noreply.github.com>
Co-authored-by: dl-alexandre <166029845+dl-alexandre@users.noreply.github.com>
* Keep the user's XDG config and state directories in platform user setup
setup-user runs with an allowlisted environment, which dropped
XDG_CONFIG_HOME and XDG_STATE_HOME. A platform's user setup reads the user's
configuration and keeps its setup markers there, so for a user who sets
either it looked in, and wrote to, the default directories instead. Both now
pass through with the rest of the session; XDG_DATA_HOME and everything else
still don't.
* Let a platform package add Hyprland defaults of its own
A platform's runtime package may need Hyprland defaults of its own (a Mac's
capture keys and three-finger workspace swipe, say), and Omarchy's config
should carry no branch for them. Three directories under the platform root,
/usr/share/omarchy-platform, now take them. The one installed platform package
owns that root, Omarchy reads it by literal path and ships nothing there, so
without a platform package nothing changes. Each file is loaded by its path,
so it can't shadow a module, and one that fails is logged and shown as a
notification while the rest of the config loads.
- hypr/defaults/*.lua loads before Omarchy's defaults. A
chord bound there replaces Omarchy's default for it, and a decorator it adds
runs before every later bind, so it can bind what has to run first. It sees
the command a bind runs, including the command a shell shortcut such as
{ menu = "root" } stands for when it binds the shell's global shortcut.
- hypr/settings/*.lua loads after Omarchy's defaults and
before the theme, for settings that replace Omarchy's.
- hypr/gestures/*.lua loads after the user's files, only for what must
see them: the gesture registry, which now records on every platform, lets a
default gesture step aside for the user's own.
The user's files load after the first two and can unbind or rebind any of it.
* Let a platform package name keys in the keybindings menu
The keybindings menu shows a key by its keysym, which on some keyboards is not
what the key says: on a MacBook, XF86MonBrightnessUp and Down are the F2 and
F1 keys, so the menu listed rows like "SHIFT + XF86MonBrightnessUp". A
platform package can now list names for such keys in /usr/share/omarchy-
platform/key-names, one "<keysym> <name>" per line, and the menu shows those
instead. The file is part of the menu cache's key, so installing or changing
it refreshes the menu. Without it the menu is as before.
Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>
Co-authored-by: kx0101 <90087463+kx0101@users.noreply.github.com>
* Keep the top bar out of a panel's camera cutout
A laptop panel with a camera notch at its top (Apple Silicon MacBooks, for
one) covered the middle of the top bar, and a bar shorter than the notch left
a sliver of every window beside the camera. Omarchy knows no panel's cutout
itself: the platform's own package describes them in
/usr/share/omarchy-platform/display-cutouts.json, by
connector, physical mode and the rows the cutout covers. On a described panel
a top bar is never shorter than the cutout, its center section sits beside
the right one, and hiding it parks all of it; [bar] notch-height sets that
floor by hand, on that panel only. Without a description, which is every
machine without such a package, the bar is as before.
Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>
* Keep PC and Intel Mac quirks off Apple Silicon
A few hardware steps and commands written for PCs and Intel Macs do the wrong
thing on Apple Silicon:
- The Intel Mac Wi-Fi quirk turns off the Broadcom firmware supplicant for
every Mac whose DMI vendor is Apple and whose chip is on its list. Apple
Silicon reports Apple Inc. through U-Boot's DMI tables, and the M1 Pro and
Max's BCM4387 is on the list, but there the firmware supplicant is the one
that works: turning it off makes firmware commands time out and breaks
scanning. The install step and its migration now skip Apple Silicon.
- fnmode=2 for Apple-like keyboards is left to the Mac's own platform package
on Apple Silicon, where the built-in keyboard's top row depends on it.
- The SPI keyboard check reads the DMI product name without failing the
hardware pass where there is none.
- The Windows VM, an x86_64 guest under KVM, runs on x86_64 only, and direct
boot (U-Boot finds the bootloader and makes its boot entries itself)
refuses Apple Silicon; the menu hides both there. Hibernation setup skips
it; its kernel has no hibernation today, and resume there is not wired.
Everywhere else each step runs as before.
Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>
* Let a platform's runtime package adjust the apps it needs to
Some apps need a platform's own touch once they are installed. On Apple
Silicon, Chromium-family browsers must decode video in software (the hardware
decoder renders green frames in them), and Steam only runs through a
FEX launcher, which then depends on Steam and must go before Steam can be
removed. Until now the Mac runtime did this with inline branches in the
installers.
Two optional dispatch operations take their place. post-install <app> runs
as the user once an installer has installed the app and finished its own
setup of it: every browser install (after its flags file, before it says the
browser is installed) and Steam's (after the package, before the 32-bit
drivers and the first launch). pre-remove <app> runs before Steam's removal
drops its packages. Where a platform registers them they refuse root (a
platform with nothing registered is a no-op, so sudo installs still work
there) and get the same home and session variables as setup-user, and a
failure fails the install or stops the removal. On Apple Silicon they
resolve in omarchy-mac's directory; every other platform registers none, so
installs and removal run as before there.
Steam's 32-bit driver step also stops failing the install on a machine with no
Intel, AMD or NVIDIA GPU (Apple Silicon, but also Snapdragon or a VM): it has
nothing to add there and now says so.
Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>
* Ask the platform's boot package before an update takes over its files
When an Omarchy package's upgrade stops only on files no package owns, the
update moves them aside and retries. On a Mac some of those files belong to its
boot chain or were written by its image (initramfs and pacman configuration,
say), and moving them aside can leave it unable to boot or update. The Mac
runtime refused those paths with an inline list.
update-takeover <path>... asks the platform's boot package instead, before
anything moves: read-only, exit 0 lets the takeover go ahead, anything else
keeps every file and the update fails as it did before the retry existed. The
update resolves it as the user and runs it with sudo only where it resolves,
inside the authorization the move needs anyway. On Apple Silicon it is
required: a Mac without its boot package, or with one too old to answer, keeps
its files. Every other platform registers none, so the takeover goes ahead as
before, asking no one.
* Install the Mac's package set on Apple Silicon
omarchy-pkg-defaults already composes install/omarchy-<platform>.packages
after the base and aarch64 lists, but Apple Silicon had no list, so a Mac got
the generic aarch64 set: no omarchy-mac or omarchy-mac-boot, whose entrypoints
the dispatcher requires there.
install/omarchy-apple-silicon.packages names the Mac's runtime and boot
packages, plus the defaults an owner may remove: the startup-volume tool,
hardware video decode, Widevine, and wf-recorder, since gpu-screen-recorder
can't capture on Apple Silicon and screen recording falls back to it. What a
Mac can't work without (its speaker stack, PipeWire's PulseAudio server and
realtime scheduling, the GPU's Vulkan driver) comes as omarchy-mac's
dependencies, so a new Mac driver needs no Omarchy change. Other platforms
compose exactly what they did.
Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>
* Give existing aarch64 machines the zram swap Omarchy configures
omarchy-settings configures swap on zram and tunes memory reclaim for it.
x86_64 gets zram-generator from its installer and new aarch64 installs from
their default packages, but an aarch64 machine installed before
omarchy-aarch64.packages named it has no generator, so the configuration asks
for a device nothing creates.
Migration 1790328426 installs zram-generator where the platform's default
packages name it and it is missing, and starts the zram swap wherever it is
down, unless no device is configured (then the generator makes no swap unit)
or the administrator masked it. A swap that won't start, or whose unit can't
be read, only warns: the generator's unit starts with every boot, so a reboot
retries it, and failing the migration would stop every update. A failed
install waits (75) for the next migrate, as it does where the platform can't
be told, changing nothing. It keeps the name it has in omarchy-mac, so Macs
that already ran it skip it. It also retires an /etc copy of the drop-in
identical to the packaged one, which earlier Mac setups made and which would
hide the packaged file's updates; an edited copy stays. On x86_64 the default
packages don't name the generator and there is no copy, so it does nothing.
* Keep an ARM machine's repositories when it changes channel
omarchy-refresh-pacman and install finalization copied x86_64 templates
everywhere, so on an aarch64 machine a channel change or a finished install
wrote Arch Linux's x86 [core]/[extra]/[multilib] and dropped Arch Linux ARM
and the platform's own repositories.
Channel changes now tell the platform apart. x86_64 is unchanged. On aarch64
the machine's configuration is kept, with its repositories, mirrors and
signature policies, and only the [omarchy] server moves to the new channel;
a configuration whose Omarchy server can't be identified is refused
unchanged, and one with no Omarchy repository gets it at the end (not on
Apple Silicon, whose own repository must not come first). Only pacman.conf is
backed up and rewritten, straight from this process through sudo, never
through a file another process could change first.
Before that, a channel must be qualified for the platform: a signed upgrade
and reboot tested there, not only a published repository. x86_64 keeps
stable, rc and edge; Snapdragon and other aarch64 machines take edge, as
dragon does; Apple Silicon takes none until the Mac packages qualify on edge.
The refresh then checks, as the user, that the channel's omarchy.db
publishes the runtime pair, and on Apple Silicon omarchy-mac and
omarchy-mac-boot. omarchy-channel-set refuses an unqualified channel before
anything else, the dev confirmation included.
Install finalization writes a whole template, as before on x86_64. Other
aarch64 platforms get Arch Linux ARM (dragon's pacman-aarch64.conf and
mirrorlist-aarch64, byte for byte) with its keyring, plus Omarchy on a
qualified channel. Apple Silicon gets omarchy-mac's template on a qualified
channel and keeps its image's configuration otherwise. In an image build,
finalization leaves the keyring to the machine's first boot, so machines
flashed from one image don't share a pacman master key. On an unqualified
channel, a machine that gets no Omarchy repository is told why.
Co-authored-by: Birk Skyum <74932975+birkskyum@users.noreply.github.com>
Co-authored-by: Jim Martin <1334144+jdvmi00@users.noreply.github.com>
* Keep Arch Linux ARM's signing keys current on ARM machines
omarchy-update-keyring reinstalls archlinux-keyring before every system
upgrade, since keys change without a version bump. An aarch64 machine's Arch
Linux ARM repositories are signed with keys from archlinuxarm-keyring, which
nothing refreshed, so a rotated ALARM key failed the upgrade that followed.
On an aarch64 platform where archlinuxarm-keyring is installed, it is
reinstalled in the same transaction. On x86_64 the keyring step runs exactly
as before, even where that package happens to be installed.
* Mark the runtime's platform files as deciding at runtime
The omarchy-settings recipe leaves the mkinitcpio, Limine, memory and USB
drop-ins out of its aarch64 package, because they used to assume x86_64. That
leaves an aarch64 install without the HOOKS baseline its initramfs needs, and
without the zram and reclaim settings every other Omarchy machine gets.
Each of those files now decides at runtime whether it applies: the HOOKS
baseline asks the platform, thunderbolt is added only where the kernel has the
module, the Limine files are read only where Limine is installed, the oomd
drop-ins are inert without systemd-oomd, aarch64 installs the zram generator
the zram drop-in needs, and the USB autosuspend option is ignored by a kernel
that builds usbcore in. default/settings-runtime-profile says so, file by
file; a recipe that finds it ships the same files on every architecture
(omacom/omarchy-pkgs#638), and older sources keep today's aarch64 package. A
test checks that every file it names exists and that each runtime decision is
in place.
* Set the wireless regulatory domain from the owner's timezone at first-boot setup
A deferred install (an Apple Silicon image, an OEM ISO install) runs its
hardware steps before the owner picks a timezone, on the UTC placeholder,
which names no country, so install/hardware/set-wireless-regdom.sh leaves the
regulatory domain unset and the radios stay on the world domain. Owner setup
now runs that step again once the owner's timezone is set, and applies it to
the radios through wireless-regdb's helper, since no reboot follows setup. An
up-front install, which picks the timezone before its hardware steps, is
unchanged.
* Ask again for a keyboard layout first-boot setup could not apply
First-boot setup kept going when the chosen layout did not take: loadkeys
errors were ignored, a keymap localectl did not list kept the default, and a
failed or partial write of /etc/vconsole.conf was only logged. The owner then
typed the disk password in one layout while the boot files asked for it in
another, and could only unlock the disk by typing US-style.
The keyboard step now fails closed. On a console loadkeys must succeed, the
keymap must be in localectl's list exactly (a list localectl cannot produce
counts as a failure too, logged apart), and after systemd-firstboot or the
localectl fallback, /etc/vconsole.conf is read back in a clean shell and must
name the chosen keymap with an XKB layout. Any failure is logged, the owner is
told the layout could not be set, and the layout is asked for again, so the
password form is never reached with a layout that did not take.
* Refresh a platform's own signing keyrings before an update
omarchy-update-keyring reinstalls archlinux-keyring (and Arch Linux ARM's on
aarch64) before the system upgrade, so a rotated key never fails that
upgrade's signature checks. A platform's own repository has a keyring of its
own that nothing refreshed: on Apple Silicon, asahi-alarm-keyring for
[asahi-alarm].
A platform package now lists those keyrings in
/usr/share/omarchy-platform/keyrings, one package name per line, and
omarchy-update-keyring reinstalls each one that is installed in the same
transaction. It adds no repository the machine didn't already trust. A line
that isn't one keyring package name is skipped with a warning, so a mistake in
the list can't stop every update. Without the file, as on x86 today, the step
runs as before.
* Drive an untyped default input from the audio panel
Quickshell types only exact media classes, so a virtual source (an
Audio/Source/Virtual: EasyEffects' source, or the microphone mapping a
platform package adds) has no PwNode.audio. With one as the default input the
audio panel showed the input at 0% and its slider changed nothing, the peak
meter stayed flat, the microphone widget read it as muted, and the panel could
not list it unless it was already the default.
Such an input now goes through wpctl for its volume and mute (UntypedInput in
shell/Commons, one reader and one pactl subscribe for the whole shell), the
panel meters it with the new omarchy-audio-source-level, and the input list
takes an untyped node PulseAudio lists as a source.
A platform whose audio runs through its own processing can also name nodes
that are neither devices nor apps in /usr/share/omarchy-platform/audio.json,
which Omarchy ships none of: the panel and the microphone widget leave them
out, and a "replaced" node only while its replacement exists. Without the
file nothing is hidden.
On every machine: the input list leaves out a source whose ports are all
unavailable (an empty headset jack) unless it is the default, as the output
list does for sinks; choosing an input no longer moves a filter's own capture
(a source output with node.link-group), which would feed the filter from the
new input or from itself; the shell's own level meters no longer light the
microphone widget; and omarchy-audio-sink-availability reads pactl in the C
locale for sinks too, since "not available" is translated.
* Give first-boot setup an XKB layout for keymaps systemd cannot map
systemd-firstboot writes an XKB layout only for keymaps listed in
kbd-model-map. Nine the setup form offers are not (Colemak, Azerbaijani,
Bulgarian, Czech, Swiss German, Kyrgyz, Norwegian, Polish, Ukrainian), so it
wrote KEYMAP= alone, the readback refused it, and owner setup could never get
past the keyboard step for them.
Setup now adds the XKB settings itself when firstboot left none, matching the
console map so the shared password types the same on the desktop: kbd's cz is
QWERTY, bg-cp1251 phonetic and azerty French AZERTY, and non-Latin layouts keep
a US layout to switch to, as kbd-model-map's own rows do. A test checks every
layout the form offers ends with an XKB layout.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>
* Leave keys a firmware key turned off dark
The ambient keyboard backlight treated any 0 the keyboard-brightness keys had
not recorded as an accident and relit it within five seconds. Firmware Fn keys
(Fn+Space on ThinkPads and Framework laptops, Fn+F10 on Dells) write the LED
directly, so on those laptops turning the keys off never stuck.
The lock screen's off now records a blank, and only that blank or a 1%
leftover is taken back; any other off is a choice that pauses auto like the
keys do. A wake clears the blank only once the restore succeeded, the command
runs one at a time so a blank racing a wake can't leave the LED and record
disagreeing, and the loop drops a blank once it has taken the keys back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>
* Drop the optional-install availability guards
Each menu open checked every Install row against the sync databases, which
costs most of a second of pacman work on every machine to hide rows only an
ARM machine is missing. The fix for those is publishing the ARM packages, not
hiding the rows, so this removes omarchy-pkg-available, the Install rows'
availability and architecture guards, and reinstall's skipping of defaults no
repository offers.
The Windows VM row keeps its x86_64 guard, since no package fixes a guest that
needs an x86_64 host, and reinstall keeps its aarch64 channel handling.
This reverts commit aea001cbde4d0852b311ef42b5e8c380c719f833.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>
* Put the current root back when the factory root cannot take its place
Factory reset moved the running root aside, marked the reset committed, then
renamed the staged factory root to @. If that second rename failed, cleanup
saw a committed reset: it skipped the boot package's rollback and kept the
throwaway LUKS slot, and since the swap had not happened it deleted the staged
root as well, leaving nothing at @.
The reset now commits only once the factory root is at @. A failed activation
renames the old root back first, then cleanup rolls the platform's boot state
back and revokes the throwaway slot. If that rename fails too, both roots are
kept, cleanup still rolls back and revokes, and the error names the rename
that recovers the machine. The generic Limine path has already rebuilt its
boot files from the factory root by then, as it had before this change.
* Leave other recorders alone when the saved recorder pid is stale
A saved pid that no longer named a recorder made status and stop select every
gpu-screen-recorder and wf-recorder the user ran, so stopping could interrupt,
and after five seconds kill, a recording started somewhere else.
A stale pid now means no recording of ours is running: the toggle starts a new
one, and --stop-recording finds nothing to stop. Stop acts on the recorder the
status check selected rather than selecting again. Without a saved pid, as for
a recording started before the pid was saved, every recorder the user runs
still counts.
* Name the audio a wf-recorder recording could not capture
With desktop and microphone audio both requested, wf-recorder records a null
sink fed by two loopbacks, and a loopback that failed to load was ignored: the
recording went ahead without that source, or silent when both failed. A
missing null sink or default device dropped audio just as quietly.
A source that cannot be captured is now left out with a notification naming
it, the loopbacks that did load are unloaded, and the recording carries the
source that remains, or no audio track at all.
* Show the Edge, Spotify and Dropbox installers on x86_64 only
These vendors currently publish Linux builds for x86_64 alone (the AUR's
microsoft-edge-stable-bin and spotify are arch=('x86_64'), and Dropbox lists
no Linux ARM client), so no native aarch64 build is available and choosing
one on an ARM machine only fails. Like the Windows VM row they keep a uname
check, which the menu answers once per open without asking pacman. The other
Install rows are unchanged.
* Revoke the generic reset's throwaway slot when the reset fails
The generic Limine path added its throwaway LUKS slot without recording it,
so a reset that failed before activation never revoked it. Once the factory
root's UKI was on the ESP, a failed activation that put the old root back left
a machine that booted the factory kernel and unlocked unattended with a key
readable on the ESP, while the screen said the current root was back.
The generic slot is now recorded like the platform's and revoked by cleanup
whenever the reset fails before the factory root is active. A failed
activation on that path says the boot files were already rebuilt from the
factory system and to run limine-update before rebooting.
* Clear a stale recorder pid when a recording starts
The recorder's pid is saved only once its file exists. Until then a pid left
by a recording that ended without a stop still counted as stale, so pressing
the toggle again while the new recorder started found nothing of ours and
could start a second recording. Starting a recording now clears that pid,
and during startup the toggle falls back to the user's recorders as before.
* Word the dropped-audio notice for every reason a source is left out
The notice said the dropped source "could not be captured", which was wrong
when the null sink itself failed to load: the microphone was never tried, and
desktop audio recorded alone as it always has. It now says the source could
not be added to the recording, true for a missing device, a failed loopback
and a failed null sink alike.
* Show the Heroic and Minecraft installers on x86_64 only
Heroic publishes Linux builds for x86_64 alone (its arm64 builds are for
macOS and Windows), and Mojang's Linux launcher is a single x86_64 tarball.
They join Edge, Spotify and Dropbox under the same uname check, so every row
whose vendor ships no native Linux aarch64 build is treated alike.
* Ask the recorder script whether its stop has a recording to end
The bar indicator and the menu's Stop Screenrecording row asked the process
helper whether any recorder the user runs was alive, while the toggle's stop
acts only on the recording it saved. With a stale saved pid alongside an
unrelated recorder, both offered a stop that did nothing.
omarchy-capture-screenrecording --status answers with the selection the stop
uses, and both now ask it. Status needs no recordings directory and never
notifies, so the recordings-directory check moved below argument parsing and
skips it.
* Copy every platform's pacman templates into place, as x86_64 does
On aarch64 a channel change edited the machine's pacman.conf in place: an awk
pass found the [omarchy] server and rewrote its channel, appended a section
where there was none, checked the channel's omarchy.db first, and gated it all
on a per-platform table of qualified channels. Install finalization appended
Omarchy to a template, or on a Mac copied one bundled inside omarchy-mac. What
a machine ended up with was never in the repository.
Each platform now has a pacman.conf and mirrorlist per channel, copied whole:
x86_64's stay in default/pacman, Snapdragon and other aarch64 machines use
default/pacman/aarch64, and Apple Silicon default/pacman/apple-silicon, with
Omarchy and Asahi ALARM ahead of Arch Linux ARM. Omarchy publishes aarch64
packages on edge alone so far, so every ARM channel points at edge for now and
channel changes work there; moving a channel is a template edit. A channel
without both files is refused before anything changes, including a dev
checkout's own templates. Reinstall goes back to resetting to stable, and the
update confirmation loses the missing-repository notice it no longer needs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>
* Revert "Copy every platform's pacman templates into place, as x86_64 does"
This reverts commit 9a07e138cc6c3a19b58d2f3d7eb536dd3042ccc5.
* Copy every platform's pacman templates into place, as x86_64 does
On aarch64 a channel change edited the machine's pacman.conf in place: an awk
pass found the [omarchy] server and rewrote its channel, appended a section
where there was none, checked the channel's omarchy.db first, and gated it all
on a per-platform table of qualified channels. Install finalization appended
Omarchy to a template, or on a Mac copied one bundled inside omarchy-mac. What
a machine ended up with was never in the repository.
Each platform now has a pacman.conf and mirrorlist per channel, copied whole:
x86_64's stay in default/pacman, Snapdragon and other aarch64 machines use
default/pacman/aarch64, and Apple Silicon default/pacman/apple-silicon, with
Omarchy and Asahi ALARM ahead of Arch Linux ARM. Omarchy publishes aarch64
packages on edge alone so far, so every ARM channel points at edge for now and
channel changes work there; moving a channel is a template edit. A channel
without both files is refused before anything changes, including a dev
checkout's own templates. Reinstall goes back to resetting to stable, and the
update confirmation loses the missing-repository notice it no longer needs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>
* Drop the migrations no installed machine needs
Fresh installs mark every migration done, and every Apple Silicon machine is a
fresh install, so the Mac handoff to omarchy-mac-boot (and the migrate
operation it called), zram for aarch64 installs that predate the package list,
the Brave Widevine link and the Broadcom migration's Apple Silicon guard never
run anywhere. The exit-75 deferral in omarchy-migrate existed only for two of
them. Keyboard backlight auto stays on new installs through first run rather
than switching on for existing ones, and migration 1786605598 goes back to
quattro: the mkinitcpio drop-ins arrive by package.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drop the generic changes Mac support doesn't need
The pacman platform guard (and the detector copy omarchy-settings shipped for
it, which the HOOKS baseline fell back to), the lint against uname gates, first
run staying pending after a failed finalization, run_logged echoing failures to
stderr, half-up battery rounding, hiding empty-jack inputs and leaving a
filter's capture alone when choosing an input all rode along with the Mac work
without serving it. Each goes back to quattro's behaviour and can come back on
its own merits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drop the platform Hyprland slots
A platform package could load its own Hyprland binds, settings and gestures:
binds that replaced Omarchy's chords and decorated every later bind, settings
ahead of the theme, and gestures after the user's files, which needed every
hl.gesture call wrapped so a default could step aside. On a Mac those carried
macOS habits (a three-finger workspace swipe, tap-to-click off, menus opening
on the built-in screen, other screenshot and keyboard-backlight chords), not
anything the hardware needs, and Omarchy should behave the same on every
machine. The one bind a Mac does need, its lid switch under the SMC's name,
joins Omarchy's own lid binds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep aarch64 machines off the release channels until a release supports them
Every aarch64 channel's template pointed at the edge repository, but a switch
to stable or rc installs the release-named omarchy and omarchy-settings. On
edge/aarch64 those are the 4.0.4 release line, which has no omarchy-hw-platform,
lifecycle dispatch or omarchy-pkg-defaults, so `omarchy-channel-set stable` on
a Mac replaced the quattro runtime it runs with one that cannot run it, and
succeeded.
aarch64 platforms now have edge templates alone, so a switch to stable or rc is
refused before anything changes; adding those templates once a release
supports aarch64 opens them. Refresh with no channel named, as reinstall runs
it, and install finalization for a channel the platform has no templates for
take the platform's default channel: stable on x86_64, edge on aarch64.
* Leave a filter's own capture alone when choosing an input again
d0b94d98c returned omarchy-audio-input-set-default to moving every source
output, as not serving Mac support. It does: asahi-audio's microphone DSP on
Apple Silicon records the raw array through a filter-chain capture stream
(audio_effect.jNNN-mic, node.link-group "filter-chain-..."). On an M1 Pro,
choosing the Mac's microphone in the audio panel moved that stream onto the
DSP's own output, and the microphone recorded digital silence (-91 dB, from
-55 dB of room noise) until PipeWire restarted. With the skip it keeps
recording (-54 dB).
This restores the skip of source outputs carrying node.link-group, and its
test, as they were before that commit.
* Say aarch64 offers edge alone where the templates are described
The aarch64 edge templates, the template helper and the update-process docs
still said every platform has a template per channel and every ARM channel
installs from edge, which the previous commit made untrue.
* Point the Mac packages at omacom/omarchy-mac-pkgs
omarchy-mac and omarchy-mac-boot now live in omacom/omarchy-mac-pkgs, with
each package at the top level. The dispatcher, its documentation and the
Apple Silicon package list name that repository, and the drop-in test's
fixtures, byte for byte the same as omarchy-mac-boot/files/etc/mkinitcpio.conf.d
at 4399105, cite it there.
* Stop naming a platform migration among the dispatcher's callers
The migrate operation went with the Mac migrations, and no caller hands a
machine to a platform migration any more, so the header lists only the
flows that still dispatch. The optional-operations comment is rewrapped.
* Keep the Intel Mac Broadcom migration off Apple Silicon again
A fresh install marks every migration done only for the user it creates, so
an account added later runs them all on its first update. On an Apple
Silicon Mac this one matches: an M1 Pro reports sys_vendor "Apple Inc." and
a BCM4387 (14e4:4433), so it would append feature_disable=0x82000, which
stops that chip scanning once brcmfmac reloads. Restore the guard and its
test case.
* Say when Steam's post-install hook runs now that the drivers go first
Steam's installer now adds the 32-bit drivers before Steam itself, so the hook runs after both and before the first launch.
* Release fallback audio after the recorder exits
When wf-recorder ends outside the normal Stop path, status rejects the saved PID before unloading the combined-audio loopbacks. Release those owned modules when the saved recorder is gone, so the next status or stop call ends microphone capture without signaling another recorder. Leave a mix without a saved PID alone: its recorder can still be starting.
Co-Authored-By: GPT-6 in Codex <noreply@openai.com>
Co-Authored-By: Codex Medium <noreply@openai.com>
* Serialize recording state changes and stale audio cleanup
Status can finish checking an exited recorder after another command has replaced its audio modules, and stop finalization can outlive the recorder while a new recording starts. Share a lock across start, stop and cleanup so each command finishes updating its resources before another changes them. Status uses a read-only check when the lock is busy, keeping the bar responsive during the picker. Close the lock descriptor in persistent children so recording and preview cleanup cannot retain it.
Co-Authored-By: GPT-6 in Codex <noreply@openai.com>
Co-Authored-By: Greptile <165735046+greptile-apps[bot]@users.noreply.github.com>
* Keep automatic and manual keyboard brightness updates together
A brightness key can update the LED and its chosen-level record after the automatic loop checks for a manual change but before it applies a new target. The automatic write then overwrites the user's choice and deletes the record. Take the manual command's lock for the whole tick, skipping a busy tick and releasing the descriptor on every return. Create the private fallback directory before opening the lock outside a session runtime directory.
The normal lock screen waits five seconds before blanking, so an ordinary tick should already see the session locked. The regression exercises a brightness key arriving within a tick instead of relying on an implausibly delayed lock-state check.
Co-Authored-By: GPT-6 in Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 High via Pi <noreply@anthropic.com>
* Read the split hook baseline in the NVIDIA migration
The NVIDIA firmware migration decides whether to rebuild by sourcing the hardware and filter drop-ins. With HOOKS now defined in the earlier baseline, that partial read treats every hybrid machine as if kms had been removed. Source the new baseline first when it exists, keeping the legacy two-file layout working and skipping rebuilds when the effective hooks still include kms.
Co-Authored-By: GPT-6 in Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 High via Pi <noreply@anthropic.com>
* Use a hardware helper for x86-only menu entries
Keep the x86_64 availability checks for Windows and vendor-specific installers in a hardware predicate, so menu conditions use the same command style as other hardware checks.
Co-Authored-By: GPT-6 in Codex <noreply@openai.com>
* Keep restored keyboard levels from pausing automatic brightness
Idle wake, quick unlock and hibernate can restore a saved level older than the ambient loop current level without the lock blanking first. Treat that restoration as a one-tick event, preserving existing manual pauses and the normal blank-and-restore path. Consume unmatched and unchanged restore records too, so a later firmware key cannot look like the earlier restore.
Co-Authored-By: GPT-6 in Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 High via Pi <noreply@anthropic.com>
* Preserve restored keyboard levels across a later lock blank
The ambient loop skips a locked session, so a restored level may still be unconsumed when the next blank saves it. Carry that origin through blanking and restore failures so fingerprint unlock cannot make the old saved level look like a manual choice. A brightness key replaces the marker with its chosen level and retains the ordinary blank behavior.
Co-Authored-By: GPT-6 in Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 High via Pi <noreply@anthropic.com>
* Preserve firmware brightness choices before a lock blank
Firmware keys change the LED without updating the record. Carry a pending restoration through the next blank only if the observed LED still matches that restoration, or is still zero from its previous blank. A firmware change otherwise follows the ordinary blank-and-restore path so automatic lighting pauses on the chosen level.
Co-Authored-By: GPT-6 in Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 High via Pi <noreply@anthropic.com>
* Change only the disk password in omarchy-drive-password
Changing the system disk's password also set the login and root passwords
to match. That changes account-password policy on every platform, x86
included, and overwrites passwords someone chose to keep different, so it
moves to its own change with an explicit confirm.
The disk change itself stays as it was: the new key must open the disk and
the old one must not before the change counts, the journal finishes an
interrupted change, and where the platform's boot checks prove the key
slots, the owner's new slot is recorded before the run finishes. The
journal's confirmed phase is now "record" and no longer needs a login user.
* Cancel a pending recording start instead of queueing behind it
Starting a recording holds the recorder lock through the picker and until the
recorder creates its file. A second toggle or a stop then waited on that lock,
with no end while a portal pick or a stuck recorder never finished, and once
the start gave up the queued toggle began another recording.
While a start is pending, a toggle or stop that cannot take the lock within a
second now leaves a cancel marker and returns. The start checks it after the
picker, after the webcam overlay and while waiting on its recorder, then stops
that recorder and releases its audio. A recorder other than the portal's that
has not created its file after 15 seconds is stopped and the next backend
tried.
* Move automatic keyboard lighting to its own change
Ambient-light keyboard backlight is a new feature on every laptop with the
sensor, not something Apple hardware needs to run Omarchy, so it leaves this
change for a separate review. The keyboard-brightness command returns to
upstream's version, and first run no longer enables the user service.
* Move workspace refocus on monitor removal to its own change
Keeping the viewed workspace in front after a monitor is unplugged changes
desktop behaviour on every machine, not just Apple hardware, so it leaves
this change for a separate review.
* Move prebuilt-image hardware deferral to its own change
Building an image away from its machine needs a manifest the detector reads,
a queue of hardware steps for the first boot, a first-boot service and a
per-machine pacman keyring. An install on the machine itself needs none of
it, so it leaves this change for a separate review with an installer and
package boundary of its own. Hardware and user setup now run where they are
installed, and the platform setup operations take no image argument.
* Move the Qualcomm platform to its own change
Snapdragon support is separable from Apple compatibility, so the detector no
longer names a qualcomm platform: a "qcom," device tree is generic aarch64,
which Snapdragon already behaves as everywhere else here, and a tree naming
both Apple and Qualcomm is still refused. linux-firmware-qcom's platform list
leaves with it.
* Keep a cancel that arrives while a recording start finishes
A stop could land after the start's last cancel check, while it saved its
recording as started, and be dropped with the marker. The start now removes
its starting marker before a final check, a request reads that marker only
after leaving its own, and one that finds the start gone waits for the lock
and acts unless the start already used its request. A recorder ended by a
cancel or a readiness timeout gets two seconds after TERM before KILL, so a
recorder ignoring TERM cannot hold the start and its lock.
* Defer an image build's hardware setup to the machine's first boot
Brings back prebuilt-image support split out of #14431 so it gets its own review. An image built away from its machine names its target in a root-owned manifest the detector reads; hardware setup is queued for the machine's first boot, run there by omarchy-provision-hardware, which also makes the machine's own pacman keyring; and the platform setup operations learn they run on an image's first boot. Apple Silicon images built by the omarchy-mac installer need this; an install on the machine itself does not.
* Rebuild the initramfs when deferred setup changes module options
mkinitcpio's modconf hook copies /etc/modprobe.d into the initramfs, but the
deferred first-boot setup only compared mkinitcpio's configuration and the
kernel command line when deciding whether to rebuild. A step that only wrote
module options, as nvidia.sh does, left the old ones in the boot image.
* Put the Omarchy repository first on aarch64, as on x86_64
quattro now searches [omarchy] before the Arch repositories (34016cfb2), and
its migration 1791403252 moves the section first in /etc/pacman.conf on every
machine. The aarch64 edge template still listed it last, so refreshing pacman
on a generic aarch64 machine would undo that migration. Apple Silicon's
template already lists it first.
* Refuse disk password changes that recovery could not settle
An interrupted change to the system disk is finished from the one key slot
the password opened. If the old password also sat in another slot, a LUKS2
change made in place and stopped before the old key was retired let the rerun
accept the new password as the old one and finish while the other copy still
opened the disk. A system disk whose current password opens more than one slot
is now refused before anything changes, naming the extra slot to remove and,
where the platform records key slots, how to record the ones kept.
The journal now notes that check (checked=1). A change journaled without it,
by an earlier version, on a disk that has other key slots may still hold a
copy of the old password, and no password entered now can show which slot is
whose, so it is left for the owner to settle by hand, in every phase, with the
commands to do it. On a disk with one slot it settles as before.
A new password that already opens a key slot is refused on every drive too:
the new key's slot is how the change is followed, and an existing copy made
the run take the wrong slot as the owner's. A settled interrupted change now
says the disk opens with the password just entered, which is true whether it
finished the change or rolled it back.
* Ignore the start marker of a recording start that was killed
A start killed before it removed its marker left it behind, and a later toggle that had to wait for the lock took that marker for a start still in progress and exited without doing anything. The marker now holds the starting process's pid and counts only while that process runs.
* Say why generic aarch64 searches Omarchy's repository first
Putting [omarchy] ahead of Arch Linux ARM follows quattro's x86_64 order and its migration 1791403252, which already reorders existing aarch64 machines. It means a package Omarchy also publishes now comes from Omarchy, and a refresh can move it there, downgrading it if Omarchy's build is older. The update-process notes and the template now say so.
* Keep a working key in the manual settlement steps
When an earlier version's interrupted change has to be settled by hand, the steps now start by finding and keeping the slot of the password to keep, test the other slots with passphrases only, never remove the kept slot, and record a Mac's slots before the journal goes. Before, removing every slot the old password opened could remove the only key when the change had not landed.
* Keep Superwhisper's x86_64-only default off aarch64
quattro now ships Superwhisper as the default dictation backend and puts
superwhisper-bin in the base list, but it has no aarch64 build. On an aarch64
machine omarchy-reinstall-pkgs and omarchy-upgrade-to-quattro asked for a
package no repository carries, Setup > Defaults offered an installer that
cannot work, new users were preset to that backend, and the dictation keys, F9
and Right Alt (AltGr on many layouts) included, were bound to it.
install/omarchy-x86_64-only.packages names the base packages only x86_64
builds. omarchy-pkg-defaults leaves them out of every aarch64 platform's set,
and omarchy-upgrade-to-quattro out of the base list it installs there. The
Superwhisper default row shows on x86_64 only, like the other x86_64-only
rows. User setup on aarch64 removes the shipped Superwhisper preset, so a new
user starts with no backend and picks one; any other selection stays.
The dictation keys are bound only once the selected backend's own program is
installed, which changes nothing where it is.
* Install the stub dictation backend where binding tests expect its keys
The dictation keys are now bound only once the selected backend's program is installed. The binding-conflict check selected Voxtype without providing it, so it no longer saw the dictation keys and failed its F9 check; the default-config check now also covers a selected backend that is not installed.
* Make the stale start marker test's dead pid reliably dead
The test killed a backgrounded sleep to get a pid that no longer runs. When the signal landed before sleep started, wait never returned and the suite timed out, about one run in three. A process that exits on its own gives the same dead pid without the race.
* Drop the virtual input level meter the panel ran through a helper
Volume and mute for an untyped input still go through wpctl; typed inputs
keep Quickshell's peak monitor. Quickshell's monitor takes typed nodes only,
so the panel hides the level bar for a virtual input rather than showing an
empty bar that reads as a silent mic.
* Find recording processes with pgrep instead of a Python /proc scan
pgrep matches the kernel's 15-character process name, so gpu-screen-recorder
is matched as gpu-screen-reco. That prefix also lets any gpu-screen-reco*
executable, or a process that renames itself, pass for a recorder, so each
match is confirmed against its /proc exe link, ignoring the " (deleted)"
suffix an upgrade leaves.
--signal re-checks that link just before signalling each recorder, which
narrows the pid-reuse window the old pidfd-based scan closed, and exits 1 when
no signal was delivered. --pid rejects values above the Linux pid limit
instead of letting bash arithmetic wrap them.
* Leave 1Password's software GL wrapper to its own change
This reverts f200105c2; the render-GPU check, Electron GL wrapper and desktop entry repair return in a separate branch.
* Keep Gliff's x86_64-only package off aarch64
quattro added gliff to the base list and a migration that installs it, but its recipe builds for x86_64 alone. It joins Superwhisper in install/omarchy-x86_64-only.packages, so aarch64 package sets leave it out, and the migration installs it on x86_64 only.
* Stub accessory enrollment in the first-boot re-key test
Owner setup now enrolls the owner's USB devices and Thunderbolt accessories after the staged LUKS key is gone, and the re-key test's provisioning run reached that step with nothing to answer it. It now stubs both and checks that enrollment runs once, after the staged unlock is retired.
* Keep USB authorization from boot off Apple Silicon
Blocking USB from the start of kernel boot writes Limine's entry-tool drop-in
and checks the Limine menu and UKI directly, but a Mac's boot chain belongs to
its boot package. omarchy-setup-security-usb-authorization --boot is refused
there before anything changes, and Setup and Remove > Security hide USB at
Boot. The boot helper itself refuses enable on Apple Silicon and treats
disable as done when nothing is configured, so turning USB device
authorization off still works on a Mac; a boot-time setting configured some
other way is left with steps to remove it by hand rather than reported
removed. USB device authorization after login is unchanged.
* Skip fixture-only checks when the shell suite runs as root
Root's locale step always writes the real /etc/locale.conf and locale.gen, and root's platform check always reads this machine, so their fixtures do not apply there. The locale test now stops after its static checks as root instead of changing the runner's locale, and the Apple Silicon refusal in the USB authorization test runs unprivileged only.
* Fetch aarch64 packages from Omarchy's HTTPS Arch Linux ARM mirror
* Name platforms x86, aarch64 and aarch64-apple
omarchy-hw-platform now prints the CPU architecture, then the hardware
family when it needs handling of its own: x86, aarch64 (every ARM machine
without a family) and aarch64-apple (Apple Silicon). A later family such
as aarch64-qualcomm or aarch64-n1x follows the same form, so aarch64*
always means ARM.
The Apple predicate is now omarchy-hw-aarch64-apple; omarchy-hw-apple-silicon
stays as a hidden alias while omarchy-mac's packages and units call it.
The Apple package list and pacman templates move to
install/omarchy-aarch64-apple.packages and default/pacman/aarch64-apple/.
Image manifests and omarchy-pkg-defaults still accept apple-silicon,
generic-aarch64 and generic from builders written before the names
settled, and the mkinitcpio baseline accepts a detector that still says
apple-silicon while omarchy-settings and omarchy update apart.
* Add omarchy-hw-aarch64 and gate architecture checks on the helpers
omarchy-hw-aarch64 is the counterpart of omarchy-hw-x86: true on every ARM
machine, Apple Silicon included. The Windows VM asks omarchy-hw-x86 instead
of reading uname itself, and the keyring refresh asks omarchy-hw-aarch64, so
an x86 update no longer runs the platform detector for it.
* Document platform names, helpers and the consistency rule for agents
AGENTS.md gains a Platforms section: the x86 / aarch64 / aarch64-<family>
naming standard, which helper answers architecture and which answers
hardware family, where platform-specific packages, templates, setup and
boot code live, and that every platform should get the same experience,
with missing packages fixed upstream before any workaround and alternative
packages kept as a last resort.
---------
Co-authored-by: Marcelo Alcantara <maralc@gmail.com>
Co-authored-by: Birk Skyum <74932975+birkskyum@users.noreply.github.com>
Co-authored-by: Jimmy Van Veen <8206925+JimmayVV@users.noreply.github.com>
Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>
Co-authored-by: Scott Jones <scottajones@gmail.com>
Co-authored-by: Marcelo Alcantara <marcelo.alcantara@bp.com>
Co-authored-by: Oliver Lukschander <33756270+oliverlukschander@users.noreply.github.com>
Co-authored-by: Caleb Marshall <68087341+cajomar@users.noreply.github.com>
Co-authored-by: Ryan Hughes <1630358+ryanrhughes@users.noreply.github.com>
Co-authored-by: Issac Trotts <15530+ijt@users.noreply.github.com>
Co-authored-by: Gabe Ohlson <3355909+6abe@users.noreply.github.com>
Co-authored-by: Sparsh Kumar <22640187+skkumarsparsh@users.noreply.github.com>
Co-authored-by: dl-alexandre <166029845+dl-alexandre@users.noreply.github.com>
Co-authored-by: kx0101 <90087463+kx0101@users.noreply.github.com>
Co-authored-by: Jim Martin <1334144+jdvmi00@users.noreply.github.com>
Co-authored-by: Ryan Hughes <ryan@heyoodle.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Omarchybot <317366263+omarchybot@users.noreply.github.com>
Co-authored-by: Greptile <165735046+greptile-apps[bot]@users.noreply.github.com>
The migration read ESP_PATH from /etc/default/limine at its top. On a
machine without that file sed fails, and under bash -euo pipefail the
migration stopped there with an error, before the check that lets a
machine without Limine skip it.
The read moves to after the rebuild, the one place the path is used; a
path given for the tests is taken first, and a missing file means /boot.
limine-mkinitcpio carries on past a kernel it could not build, with a
full boot partition for one, and still exits 0. The migration then wrote
its marker over an unchanged boot image, and no later update would try
again.
After the rebuild it now looks at the boot menu: if the menu has command
lines and none carries the parameter, the migration fails and stays
pending. A menu with no command lines in it, where they are inside the
boot images, cannot be checked this way and is accepted as before.
The test ran the migration with plain bash. omarchy-migrate runs it with
bash -euo pipefail, and the migration depends on that: it is what stops
a failed limine-mkinitcpio before the marker is written, so the next
update tries again. The stand-in rebuild could not fail, so nothing held
that promise.
The test now uses the same options and adds the case: a rebuild that
fails ends the migration with an error and leaves no marker, and the
next run rebuilds and marks it.
About 1 boot in 10 of an installed system stops on a black screen at
"Starting Switch Root" until a key is pressed. systemd 257 and later
asks the console for its size with ANSI escape sequences when PID 1
starts and waits for the reply; with Plymouth holding the VT the reply
sometimes never comes.
Measured in QEMU on repeated boots of one install: 3 hangs in 30 and 1
in 40 on the prebuilt UKI, 7 in 40 on the stock initramfs, every one of
them ended by pressing Enter; none in 40 with a serial console, none in
40 with plymouth.enable=0, and none in 80 with
systemd.tty.term.console=dumb, which makes systemd skip the query.
systemd fixed two variants of this in v261; Arch's 261.2 still shows it.
The parameter goes where the last Plymouth boot fix went (3f725531,
initramfs_async=0): into the packaged omarchy-defaults.conf for fresh
installs, and a migration rebuilds the boot image on existing systems,
once per machine, only where the booted command line lacks it. Nothing
visible changes under "quiet splash".
Refs #2931
* Install gliff by default
Gliff is the Hyprland remote desktop over SSH. Ship it in the base
package set for new installs, with a migration for existing ones.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Show a bar indicator while a gliff remote session is active
The machine being controlled should make it obvious that someone is on
it. A first-party remote-session service watches Hyprland's screencast
events, which gliff-server triggers through ext-image-copy-capture, and
probes for the server process, picking up the ssh client address from
its environment. The RemoteSession indicator in the default tray lights
up in the urgent colour while a session runs, and a notification
announces each session start and end.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Address review of the remote session indicator
Probe only the current user's gliff-server processes, forward refresh
through the third-party service proxy so cloned indicator widgets can
re-probe, exercise the Hyprland screencast event path in the service
test with a grim capture instead of calling refresh by hand, and
describe the indicator as status only in the manual.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Drop the remote session notifications
The bar indicator is the signal; a toast on every connect and
disconnect is noise on top of it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Tighten remote session detection after review
Hyprland reports a screencast stopped as soon as frames pause for half a
second and says nothing more when the server later exits, so poll every
two seconds while a session is active instead of trusting a stop event.
Match only the screencast event, not its screencastv2 twin, and debounce
the burst around one transition so it costs one probe. Move the probe
into its own script that the service and the test both run, poll for
state in the service test instead of fixed delays, and drop the unused
session count.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Hyprland skips a release bind once any other key or mouse button is
released while its key is held (KeybindManager::shadowKeybinds), and
when the modifiers changed since the press. That is what makes a bind
like "bindr = SUPER, SUPER_L" tap-only, but it means typing or clicking
while holding F9 or Right Alt never runs omarchy-dictation stop, so the
recording keeps going.
o.bind_hold binds a press and a release action together. The release
half is transparent (it can't be shadowed), ignores modifiers, and is
non-consuming, so it doesn't take the key from apps when pressed with
other modifiers (Shift+F9 still reaches the app). It runs only after its
own press ran, so releasing the key without the rest of a chord does
nothing. Dictation's push-to-talk keys use it.
Co-authored-by: superduck00 <294879536+superduck00@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Delete extra themes from the theme picker
Delete on an extra (user-installed) theme opens the shared faded confirmation dialog; confirming runs omarchy-theme-remove and selection lands on the theme before the deleted one. Stock themes and user overrides of them are not offered for deletion, apply/re-ask are blocked while a removal runs, and row refreshes queue so a deletion never leaves the list stale.
* Fail closed on theme deletion until stock themes are known
Keep Delete disabled until the stock listing finishes successfully, so a stock override cannot look deletable in the startup window. Also hold theme apply (and further delete asks) from removal exit until the refreshed rows load, so Enter cannot apply the just-deleted theme in the gap.
Remove the carousel's fade and geometry transitions, hold the reveal until every visible preview has decoded and two frames have pre-rendered (falling back to revealing anyway after 400ms), and drop the fullscreen scrim and its theme tokens so the picker sits directly on the desktop. Only Escape is accepted before the picker is visible.
* Add Starship as a default theme
Adapted from Starship To Orbit by Erik Johansson (@erikrjohansson).
https://github.com/erikrjohansson/omarchy-starship-to-orbit-theme
Copyright (c) 2026 Erik Johansson.
Retain the basic palette and three SpaceX launch photographs, with
standard Omarchy styling. Compress the backgrounds to WebP while
preserving their full 6016 x 3384 resolution.
The photographs remain copyright SpaceX and are not covered by MIT.
* Add engine cluster background to Starship
* Use Flight 14 dawn and reorder Starship backgrounds
* Fix lopsided cursor highlight on network panel header actions
The QR and speed-test header buttons used raw Button components with
asymmetric horizontal/vertical padding, producing a highlight box wider
than tall around their near-square icons. Swap them for
PanelActionButton, the canonical square inline-action component already
used elsewhere in the kit (e.g. connectPwBtn), which sizes itself
symmetrically around the icon.
Fixes#8782
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Center icons in PanelActionButton on whole pixels
A fractional size and anchors.centerIn put the glyph on half-pixel
coordinates, so it rounded a pixel off the center of the fill: the
speed-test icon sat low in the network header. Round the size, make it
odd so odd-width glyphs get even margins, and place the icon on whole
pixels.
Co-authored-by: Fabian Behrendt <7931874+fabianbehrendt@users.noreply.github.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Fabian Behrendt <7931874+fabianbehrendt@users.noreply.github.com>
The existing test runs mise-work.sh in the runtime context, which skips
the bundled tarball altogether. This offers both tarballs to an x86_64
and an aarch64 machine and checks that each unpacks its own and hands
Mise that version, and that a machine with only the other architecture's
tarball fails. Against the old script the aarch64 case fails.
An aarch64 ISO bundles node-v*-linux-arm64.tar.gz, and mise-work.sh
looked for node-v*-linux-x64.tar.gz on every machine. An offline install
on aarch64 stopped at its last step: "Error: bundled Node.js tarball
missing from /opt/packages".
The name now follows uname -m. x86_64 looks for the same file as before.
This is the change the n1x branch already carries, from Spencer Bull's
6d022e36.
- 🐛 leaving dev refreshes stable or rc with the checkout's tools, so a profile missing those fragments would drop regional repositories
- ✅ refuse a profile missing any single fragment
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
- ✨ omarchy now runs per-channel china replicas, so they should lead instead of trailing as a fallback
- ♻️ prepend per-channel fragments so each channel's own mirror stays the fallback
- 🔥 drop the ustc arch mirror, which was never a stable or rc snapshot replica
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
* Add selectable dictation backends
* Move dictation migration after repository priority update
* Extract backend-specific dictation installers
* Keep dictation installer dispatcher executable
* Call dictation backend installers directly
* Drop legacy Superwhisper migration and runtime cleanup
* Separate dictation defaults from generic recording protocol
* Add backend-specific dictation removers
* Preserve existing Voxtype selection during upgrade
* Make dictation backend selection safe and retryable
* Show the dictation indicator only for selected Voxtype
* Allow switching away from stopped Superwhisper
* Remove the built-in bar dictation indicator
* Store default dictation selection under defaults
* Rename dictation backend selector to use
* Move dictation removers into their own menu category
* Play matching theme background intros at boot
* Add Tokyo Night sunset lake boot intro
* Stage Tokyo Night boot intros on update
* Add user controls for background boot intros
* Optimize Tokyo Night boot intro videos
* Add Tokyo Night cityscape boot intro
* Add Ristretto boot intro videos
* Add Solitude boot intro videos
* Add White boot intro videos
* Add Vantablack boot intro videos
* Add Miasma nature boot intro
* Add Matte Black boot intro videos
* Add Lupine wave boot intro videos
* Add Last Horizon blink boot intro
* Add Everforest tree tops boot intro
* Add Ethereal cosmic boot intro
* Add four Retro 82 boot intros
* Add Tokyo Night Quattro boot intro
* Add Catppuccin Latte color fade boot intro
* Add five Gruvbox boot intros
* Expand Retro 82 boot intro coverage
* Complete Ethereal boot intros
* Complete Retro 82 boot intros
* Stage boot intros for the active theme
* Add Catppuccin boot intros
* Complete Tokyo Night boot intros
* Expand Lupine boot intros
* Add Osaka Jade mountain boot intro
* Prime boot intros before revealing wallpaper
* Complete Osaka Jade boot intros
* Keep kernel migration test valid after timestamp reuse
* Shorten packaged boot intros to six seconds
* Revert "Shorten packaged boot intros to six seconds"
This reverts commit 5530403aa9e789b977a1a1a77ea7acf37a9e3598.
* Document five to seven second intro target
* Ease Matte Black intros into six seconds
* Play boot intros through OWE instead of a shell player
The merged desktop video path removes BackgroundVideo.qml and makes
BackgroundMedia image-only. This keeps the intro feature by resolving the
matching intro as before and playing it with 'owe intro', which owns the
decode and the reveal. OWE 0.2.3 adds that command. The shell holds the
theme color while resolving and cancels with 'owe intro-stop'.
* Make boot intro startup atomic
* Retry boot intro through login window
* Document required OWE intro protocol
* Add Last Horizon video boot intros
* Play boot intros with released OWE
Use `owe intro` instead of the unreleased intro-prepare/intro-commit
protocol from omacom/owe#5. The boot is recorded before playback, and
OWE's intro_result status decides whether a failed intro never started
(release the boot for retry) or was interrupted (keep it consumed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Replace Matte Black boot intros with shorter cuts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Hide the still behind black until the boot intro starts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Shorten the Everforest boot intro
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Document that boot intros should open dark
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Refine the Osaka Jade boot intros
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Skip compositor fades on the background layers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Shorten the Catppuccin Latte boot intro
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Shorten the Catppuccin boot intros
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Shorten the White boot intros
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Shorten the Vantablack boot intros
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Shorten the Gruvbox boot intros
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Shorten the Retro 82 boot intros
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Shorten the Solitude boot intros
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Rework the Ristretto boot intros
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Shorten the Tokyo Night boot intros
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Complete the Lupine boot intros
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Complete the Last Horizon boot intros
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Complete the Miasma boot intros
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add the Nord boot intros
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add the Rose Pine boot intros
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add the Flexoki Light boot intros
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add the Hackerman boot intros
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add the Kanagawa boot intros
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add the Lumon boot intros
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fade in the Last Horizon intros from black
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Animate the Catppuccin boot intros from their wallpapers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KAPp37DLjmKQy8GZr3vFvF
* Animate the third White boot intro from its wallpaper
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KAPp37DLjmKQy8GZr3vFvF
* Animate the Flexoki Light, Lumon and Rose Pine plants intros
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KAPp37DLjmKQy8GZr3vFvF
* Shorten the Ethereal boot intros
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KAPp37DLjmKQy8GZr3vFvF
* Animate the remaining placeholder intros from their wallpapers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KAPp37DLjmKQy8GZr3vFvF
* Replace Catppuccin intros with themotion scenes
Totoro moonrise, waves swelling from a line, and the blue eye tracing
itself open, rendered by https://github.com/yamz8/themotion
(scenes/catppuccin/*).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KAPp37DLjmKQy8GZr3vFvF
* Re-encode Catppuccin intros with exact final frames
Same motion; the settled frames are encoded at a fixed high quality so
the handoff lands on the wallpaper (themotion 0c5e86c).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Replace Lumon intros with themotion scenes
United in severance draws its lines and globe with a small pen-tip
light; Opinions equally wakes inside the picture's own screen and types
out the mark and tagline. Both keep the wallpaper's proportions, so the
handoff holds its shape. Made with https://github.com/yamz8/themotion
(scenes/lumon/*).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Land the Last Horizon intros exactly on their wallpapers
Same footage and timing; the last 0.4 s now blends into the wallpaper as
the shell draws it (libwebp decode, BT.709 limited range), with the
settled frames at high quality. Re-finished with themotion fit (09f156e).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add Nord video boot intro for Night Hawks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Replace Rose Pine intros with themotion scenes
Funky shapes pop in on a jelly spring and the doodles draw on (replacing
the reversed stock clip), the dot map spins in from a swelling dot, and
the plants spring up before the OMARCHY letters drop in. Made with
https://github.com/yamz8/themotion (scenes/rose-pine/*).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Replace Hackerman intros with themotion scenes
The synthwave terrain unfolds as the sun rises and the valley glow
flares into the floor; the geometric network bursts out from a triangle
at its core and locks into place. Made with
https://github.com/yamz8/themotion (scenes/hackerman/*).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Replace White 3-white with a themotion scene and land 1-2 exactly
3-white folds and slides its paper sheets into place along their
diagonal (themotion scenes/white/3-white). 1-white and 2-white keep
their footage and timing; their last 0.4 s now blend into the wallpaper
as the shell draws it (themotion fit).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Replace Ristretto 0-1 with themotion scenes and land 2-3 exactly
0-launch lifts its stripes off and bends them into their curves, and
1-color-curves fans its colour bands in along their curves (themotion
scenes/ristretto/*). 2-coffee-beans and 3-industrial-moon keep their
footage and timing; their last 0.4 s now blend into the wallpaper as
the shell draws it (themotion fit).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Replace Vantablack intros: new layer scenes, shared dot-hands cut
0-dot-hands uses the Matte Black cut, since both themes ship the same
wallpaper. 1-twisted-stairs keeps its footage and lands exactly on the
wallpaper (themotion fit). 2-layers-deep opens its depth front to back
and 3-layers-stacked lays its papercut stack down sheet by sheet
(themotion scenes/vantablack/*), replacing footage that opened on
unrelated shapes and a plain fade-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drop the shockwave ring from the Totoro intro
The moon now opens from its seed of light with only its halo; the thin
ring racing outward read as clutter (themotion 6970484).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drop the shockwave ring from the blue-eye intro
The eye closes with its pulse and halo only (themotion, same change as
Totoro's).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Land the earlier footage intros exactly on their wallpapers
Everforest, Osaka Jade, Catppuccin Latte and two Matte Black intros
were finished before the exact-ending tooling and ended 0.9-4 levels
off their wallpapers (up to 134 on small spots). Their footage and
timing are unchanged; the last 0.4 s now blend into the wallpaper as the
shell draws it (themotion fit).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fade two Matte Black intros up from black
ship-at-sea and dark-waters opened at brightness 17-20, so they popped
in after the black cover. They now fade up over 0.3 s; the cuts and
their exact endings are otherwise unchanged (themotion fit --fade-in).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drop the Flexoki Light intro for now
Its orb scene isn't good enough yet, so Flexoki Light starts on its still
until a better intro is ready.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit bfe60d6c0a7a0983509a2dc2b295693bec2ceeb8)
* Skip boot intros while animations are off
A VM starts with animations off, and there Hyprland renders on the CPU, so a
video intro costs the most. The launcher now consumes the boot without playing
when Hyprland reports animations:enabled false, and the shell's cover check
treats that, like the intros toggle, as nothing to wait for.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add Miasma video boot intro for Crowned
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep boot intro startup outside the background service
OWE destroys and recreates the background plugin during playback. Keep the launcher and black cover in the persistent shell host, consume the boot before waiting briefly for OWE, and start on the video's first frame. The startup check runs with a video background too, so enabling the still background service later cannot play an intro over an established desktop.
Co-Authored-By: Codex Medium <codex@openai.com>
* Release the boot marker lock before waiting for OWE
The marker settles startup once it is written. Keeping the lock through readiness and playback makes a fresh shell wait for an older launcher before it can discover that the boot was consumed, leaving the still covered during that wait.
* Keep the boot intro cover above native and cloned backgrounds
Cloned backgrounds receive a scoped shell interface without the startup coordinator. Keep the input-transparent cover in the persistent host so every background stays hidden until OWE has a frame, without widening the plugin interface.
Co-Authored-By: Codex Medium <codex@openai.com>
* Map the startup cover only for an unsettled boot
The cover must know the boot was already consumed before its first frame, so restarting the shell during playback leaves OWE visible. Snapshot the marker without watching subsequent writes: the launcher consumes this boot before the intro starts, while the cover remains needed until the first decoded frame.
* Exercise automatic boot cover release in the clone test
The fixture hid the cover itself before the fake launcher exited, so a broken exit handler still passed. Let the launcher finish before checking the actual background pixels and leave cover release to the service.
Co-Authored-By: Greptile <165735046+greptile-apps[bot]@users.noreply.github.com>
* Finish the Miasma, Kanagawa, Ethereal and Nord intros
Start each intro on its scene instead of fading in from black, since
OWE now starts intros on their first frame.
- Miasma Crowned: ejuro's intro, shortened to 6.7 s and opening on the lit hand
- Nord Night Hawks: ejuro's intro
- Kanagawa: rendered in the wallpaper's own shape, so the handoff matches on
16:10 and 3:2 screens too
- Ethereal, Nord and Miasma Crowned: 1080p endings that blend into the exact
wallpaper, so the handoff no longer sharpens visibly
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Finish the Tokyo Night intros
Start each intro on its scene instead of fading in from black, since
OWE now starts intros on their first frame.
- Winding Road: a new starry night to sunset with no added clouds or sun,
ending on the wallpaper
- Quattro: a new take where the car lands in the wallpaper's exact pose,
colour-matched to the wallpaper
- Oma Cityscape: drop the second, larger moon that faded out at the end
- Quattro, Swirl Buck, Omakub and Oma Cityscape: 1080p endings that blend
into the exact wallpaper, so the handoff no longer sharpens visibly
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Pair background intros by filename and play them on theme switches
Keep matching clips in backgrounds/intros so the selected image determines playback without registration, overrides, or checksum sidecars. Retain one global toggle and play automatically at boot and on a different theme selection.
Launch theme playback after releasing the selection lock. Check the staged theme directory identity so an older launcher cannot play a newer selection when filenames are reused. Skipped or failed playback immediately hands the selected still to the shell, including new pixels behind an unchanged path.
Synchronize the clone screenshot fixture with capture instead of a fixed launcher lifetime, while still requiring the real exit handler to release the cover.
Co-Authored-By: Codex Medium <codex@openai.com>
* Settle pending background colors when applying the current theme
An intro applies its theme immediately while a previous still-image reveal may still hold an older palette. Replace and consume that pending palette through the background's existing handoff methods so its reveal or fallback timer cannot restore old colors. Reusing those methods also works with existing background clones.
Publish the fake renderer's PID before announcing playback readiness, so the lock test cannot read an unwritten PID file. Regressions reproduce both failures on 7b238cf1 and pass with the fixes, including a deliberate scheduling delay at PID publication.
Co-Authored-By: Greptile <165735046+greptile-apps[bot]@users.noreply.github.com>
* Use the existing animations menu toggle for background intros
Keep the separate intro command available without another Style menu control. Disabled animations already suppress playback at boot and on theme changes.
Use remembered backgrounds with matching clips when testing suppressed theme intros, and require the normal still transition so the checks cannot pass merely because no clip was selected.
Co-Authored-By: Codex Medium <codex@openai.com>
* Finish the Lupine intros
Start each intro on its scene instead of fading in from black, since
OWE now starts intros on their first frame.
- Cherry Blossom White: a new take that pulls back from fresh leaves to the
whole tree, easing to a stop on the wallpaper
- Pastel Clouds: drop the unrelated starry opening and its dip to black
- Elegant Blue Wave: drop the empty white opening
- Cherry Blossom Bokeh, Pastel Clouds, Elegant Blue Wave and Cherry Blossom
White: 1080p endings that blend into the exact wallpaper
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JBE1V6gjrqn2aXCNNGhrUY
* Synchronize theme intro and bar crossfades
* Start theme intro fades from a cached first frame
* Open theme intros on moving scenes
* Start theme crossfades promptly
* Prepare theme intros before switching
* Finish the Catppuccin intros
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JBE1V6gjrqn2aXCNNGhrUY
* Keep theme intro handoffs covered
* Reload Hyprland after theme intro playback
* Finish the Rose Pine intros
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JBE1V6gjrqn2aXCNNGhrUY
* Restore the wallpaper early on each login
* Keep the startup wallpaper cover transparent
* Wait for readiness in the intro lifecycle test
* Consume login intros for updates outside Hyprland
* Fade login directly into the ready desktop intro
* Sharpen Quattro, Swirl Buck and Oma Cityscape
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep compositor backgrounds black during login
* Keep the shell visible through compositor shutdown
* Bound the login cover when startup media fails
* Encode the 1440p intros at H.264 level 5.1
At 2560x1440 the previous encodes used 16 reference frames, which puts
them at level 6.0. Intel hardware decoding plays those with stray grey
blocks. Four reference frames keep them at level 5.1, like the other
intros. Swirl Buck's stag now changes colour as one silhouette instead
of leaving a shrinking black disc on its head.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Hide the startup cursor until the desktop fades in
* Restore Swirl Buck's original stag
The stag now changes colour exactly as in the original clip again;
the previous edit made it jump from black to purple in one frame.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Initialize the startup cursor before the first compositor frame
* Render the Hackerman intros at 1440p
Both open on their first motion instead of fading in from black.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Render the Lumon intros at 1440p
Both open on their first motion instead of fading in from black.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sharpen the Lupine intros
Upscaled from the original 720p clips and exported at 1440p with a
gentler encode, keeping the same frames, timing and endings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Resolve login intros before the wallpaper daemon starts
* Synchronize cold-start captures with the fixture phases
* Sharpen the rest of the Tokyo Night intros
Winding Road and Sunset Lake are regenerated natively at 1440p from
their wallpapers, since their original clips were 360p and 720p.
Omakub and Oma are redrawn from their wallpapers with the same motion.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Bjarne Oeverli <1419214+bjarneo@users.noreply.github.com>
Co-authored-by: Erik Johansson <172146456+ejuro@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Omarchy Bot <omarchybot@users.noreply.github.com>
Co-authored-by: Codex Medium <codex@openai.com>
Co-authored-by: Greptile <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
* Let users choose passwordless sudo duration
* Warn in the menu bar when passwordless sudo is active
* Fix sudo indicator hover behavior and repeated authentication
* Disable passwordless sudo from the bar without a terminal
* Shorten passwordless sudo indicator tooltip
* Recover interrupted passwordless sudo duration switches
* Allow sudo grant changes when listings require authentication
* Start passwordless sudo setup with the duration question
The manual still said the panel shows the percentage used and that an idle
account keeps its last known limits, and the README that sign-in trouble is
only text. Rows show the time to reset with the percentage on hover, and a
lapsed sign-in is a Sign-in required link that signs the account in again.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Slack is one of the most popular apps people install from the AUR. It now
ships from the Omarchy package repository as slack-desktop, with native
x86_64 and aarch64 builds. Install it the same way as Bitwarden: one menu
entry that installs the package and then launches Slack. The commercial
apps page of the manual gets a matching section.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each pending user's one-time migration requests a reboot even when hardware setup has already installed the aliases. Let migration completion handle repeat runs and remove the shared marker and its privileged write.
Co-Authored-By: Greptile <165735046+greptile-apps[bot]@users.noreply.github.com>
Keep hardware setup limited to installing the firmware package. The migration owns the pending-reboot marker and preserves prompts for every user migrating before reboot.
Install the separately owned firmware aliases through the package helper so stock firmware updates and stable refreshes retain them. Use a distinct migration to reach machines with the prior migration completed and preserve the reboot prompt until reboot. Restore the generic updater and leave display quirks to the kernel.
Co-Authored-By: Codex GPT-6.1-Sol XHigh <noreply@openai.com>
- ✅ The channel and update boundary tests copied the real render and dev-region guard, so on a China install they read the host's `/etc/omarchy/region` and failed before reaching their checks
- ♻️ The shared fixture now points both lookups at a sandbox target that starts global, and the regional test only writes `cn` there
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdXqTwm63rRMwotZ1xqLC8
- ✅ the render read the host's region marker, so a global machine never rendered China and a China machine failed on missing fragments
- ✅ check the finished China files root installs, not only the copy order
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdXqTwm63rRMwotZ1xqLC8
- 🐛 a failed render, a refused sudo prompt, or a signal left the scratch directory behind; repeated attempts piled them up
- 🔒 the cleanup runs before the sudo revocation, which stays the last step of the exit path
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdXqTwm63rRMwotZ1xqLC8
- 🔥 keep this change to the regional build architecture; Chinese input gets its own proposal instead of fixing one engine here
- ✅ assert the shipped profile adds only its repository keyring
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3Jna1ogUUviYLJzhsG6Vr
- 🔒 upstream refresh now gives root only command-scoped copies, so regional files are rendered as the caller and installed with the same four `sudo cp` calls
- ✅ exercise refresh only inside the sudo-boundary sandbox; the old host-level test reached real sudo once refresh pinned its PATH
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 📝 the menu bypasses the input method, so requiring Pinyin there made the release procedure impossible until upstream fixes it
- 📝 record the required `skel` profile and the dev-switch region check
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 🐛 refresh used to copy over the existing files, so a customized mode must survive the shared regional applicator too
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 🛡️ refuse a dev checkout that lacks this machine's region profile before linking it, since its own refresh would silently replace the regional pacman files
- ✅ isolate channel tests from the host region marker
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ✅ protect global behavior and regional mirror ordering
- ⌨️ guard personal settings and reserved input shortcuts
Amp-Thread-ID: T-01a0df38-0ea7-759b-bcff-29da223cc567
Honor package exclusions while returning failure when an installation leaves the required Cirrus firmware too old. Print-only and download-only operations remain successful without a repair or reboot request.
Co-Authored-By: GPT-6.1-Sol High <noreply@openai.com>
Co-Authored-By: Greptile <165735046+greptile-apps[bot]@users.noreply.github.com>
Constrain Cirrus firmware in the package transaction so a stable channel refresh cannot remove the required aliases. Recover old firmware independently of completed migrations, and request reboot only after verifying a successful repair. Preserve package exclusions through native pacman handling while keeping interactive conflict recovery answerable.
Co-Authored-By: GPT-6.1-Sol High <noreply@openai.com>
Co-Authored-By: GPT-6.1-Sol XHigh <noreply@openai.com>
* Bound clipboard capture reads
* Bound the watched clipboard read and drop copies cut off at the deadline
In watch mode capture.sh reads the copy from the owner's pipe on stdin, to EOF, with no bound. An owner that stalls without closing its end keeps the callback alive, and wl-paste --watch handles no further clipboard events until it exits. That matches #9443, whose stuck capture.sh had no wl-paste child: --list-types never reads from the owner.
Every read now goes through one bounded reader into a temporary file, and the copy is recorded only when the read finished, so a snapshot that stalls halfway no longer records its first half.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Omarchybot <317366263+omarchybot@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Add Install > Service > Microsoft with Outlook, Office, and Teams web apps
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Prefix the Microsoft web apps with the brand so they sit together in the launcher
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Open the Office web app at the sign-in page
office.com now forwards to the Microsoft 365 marketing site when signed out, while
Outlook and Teams open straight to sign-in. The login path does the same for Office.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Add Microsoft OneDrive to the Microsoft web apps
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Point the Teams web app at teams.cloud.microsoft
Microsoft moved Teams to its cloud.microsoft domain; teams.microsoft.com is the
legacy address now.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Document Microsoft web app customization
Explain individual app selection, organization-specific URLs, and a separate browser profile using existing launcher options. Keep the optional bundle separate from the manual's default apps.
Co-Authored-By: GPT-6 in Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 Medium <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Omarchy Bot <omarchybot@users.noreply.github.com>
Co-authored-by: GPT-6 in Codex <noreply@openai.com>
Recent runtime changes left the aggregate suite expecting a VS Code symlink and fixed extension version, pacman arguments without the end-of-options marker, a one-argument QML text signal, and background functions without preparation state. Align these fixtures with the current interfaces while retaining palette equality, package retry, keyboard action, and missing-background recovery assertions.
Co-authored-by: Omarchybot <317366263+omarchybot@users.noreply.github.com>
* Retry systemd reload after installing fingerprint recovery
A failed reload leaves the migration pending with the drop-in already installed. Reload existing configuration on retry so completion means systemd has applied the stop timeout, while preserving administrator changes.
Co-Authored-By: Codex Medium <317366263+omarchybot@users.noreply.github.com>
* Avoid sudo for an already-applied fingerprint repair
Migration completion belongs to each user, while the systemd repair is machine-wide. Query systemd reload state without elevation and reload only stale configuration, so interrupted repairs still retry and later users can complete without sudo. A failed state query remains a migration failure.
Co-Authored-By: Greptile <165735046+greptile-apps[bot]@users.noreply.github.com>
---------
Co-authored-by: Omarchybot <317366263+omarchybot@users.noreply.github.com>
Co-authored-by: Greptile <165735046+greptile-apps[bot]@users.noreply.github.com>
* Restart fprintd after resume to clear a claim wedged by suspend
A fingerprint verify still open when the machine suspends leaves fprintd
unable to hand the reader back: the verify dies with "Cannot run while
suspended" and the follow-up ReleaseDevice fails on the still-busy device.
The wedged claim then rejects every lock-screen attempt after resume until
fprintd exits on its own 30-second idle timer -- and the retry loop keeps
it from ever reaching that timer, so the reader stays dead until the user
gives up and types a password.
Install a system-sleep hook that restarts fprintd on resume, dropping the
claim so the reader answers on the first touch. It is installed by
omarchy-setup-security-fingerprint and removed by its teardown, so it is
present exactly when a fingerprint reader is configured. try-restart is a
no-op when fprintd is not running, so a healthy resume pays nothing.
Approach suggested in #7229 and measured by @paracycle: 45 stray PAM
sessions after resume down to 2.
* Pace fingerprint retries and show when the reader is unavailable
The lock screen retried fingerprint auth on a flat 250ms timer with no
sign to the user, so a reader it could not reach -- a claim wedged across
suspend, one held by another client, or a sensor gone from the bus --
spun PAM sessions at four per second behind an icon still inviting
touches that could never unlock.
Pace and report on one signal: whether an attempt reached the reader at
all. pam_fprintd relays a finger prompt only once the claim lands, so an
attempt that ends without prompting never reached the device. Those
advance a streak that backs the retry off exponentially (to a ceiling
above fprintd's 30s idle exit) and, past a few in a row, crosses out the
icon and shows a "Fingerprint reader unavailable" notice. An attempt that
did prompt proves the reader works -- a finger that merely did not match
still reaches it -- so it clears the streak and the loop stays responsive.
User presence (a keypress or touch) collapses a backed-off wait to a
prompt retry, rate-limited so a moving cursor cannot respin the storm. An
attempt that never reaches the reader within a few seconds is aborted and
settled as unreached, so a claim orphaned by the resume restart surfaces
the notice and retries a fresh daemon rather than hanging silently.
The pacing, streak, nudge, and reach-timeout logic live in
FingerprintModel.js with Node coverage; the new Text elements declare
textFormat; lock status reports fingerprintUnavailable.
The attempt state machine tracks the open attempt with fingerprintAuthenticating alone; the first settle closes it, and one PAM attempt raising both onError and onCompleted still folds into the streak exactly once.
Claude-Session: https://claude.ai/code/session_0168egYTXrVBVg16ugszGzQt
* Install the fprintd resume hook root-owned and keep it with the PAM file
cp -p carried the checkout's owner and mode into
/usr/lib/systemd/system-sleep/, so under dev-link the root-executed hook
was user-owned, and a tree whose exec bit had been stripped installed a
hook that systemd-sleep silently never ran. Use install -Dm755 -o root
-g root, as the migration that installs the same file already does.
The hook also belongs exactly where the fingerprint PAM file does:
omarchy-apply-lock creates and removes /etc/pam.d/omarchy-lock-fingerprint
on its own, and any apply-lock run after enrollment left PAM without the
hook while its removal branch left a hook behind without PAM. Have
apply-lock install and remove the hook together with the PAM file, and
teach apply-lock-test.sh to redirect the hook into its scratch tree and
assert the hardened run lands it beside the PAM fixtures.
Claude-Session: https://claude.ai/code/session_0168egYTXrVBVg16ugszGzQt
* Only treat fingerprint as configured when a print is enrolled
The lock screen and omarchy-apply-lock decided fingerprint was set up with
fprintd-list | grep -qi finger, which also matches "has no fingers enrolled"
and "ListEnrolledFingers failed". A second account on a machine where one
user enrolled, or anyone who ran fprintd-delete, was therefore handed the
fingerprint loop: every attempt bailed before the claim, and with the new
pacing that showed up as a crossed icon and "Fingerprint reader unavailable"
for a reader the account simply has no print on. Match the per-print
" - #N:" lines instead.
apply-lock-test.sh follows: its fprintd-list stubs answer with a real enrolled-print row and its helper patcher matches the new probe line.
Claude-Session: https://claude.ai/code/session_0168egYTXrVBVg16ugszGzQt
* Exercise the migration's default hook source in its test
Every case overrode OMARCHY_FPRINTD_RESUME_SRC, so the path the migration
really reads from was never checked, while its -f guard turns a missing
source into a clean exit and a permanent per-user marker. Add a case that
runs against the shipped hook under the repo, and adopt set -euo pipefail
like the sibling tests.
Claude-Session: https://claude.ai/code/session_0168egYTXrVBVg16ugszGzQt
* Take the fprintd restart off the thaw and bound its stop timeout
The resume hook ran systemctl try-restart synchronously while user
sessions were still frozen, so its cost landed on the wake path: half a
second when fprintd answers SIGTERM, but a wedged fprintd on a stale
device handle (the reader re-enumerated across the sleep) does not, and
then the desktop stayed frozen for the whole stop timeout -- precisely in
the case the hook exists for.
Enqueue the restart with --no-block instead, as the unmount-fuse hook
already does for the same reason, and ship a drop-in capping fprintd's
TimeoutStopSec at 3s so the restart lands within seconds either way. The
drop-in is numbered 10-stop-timeout.conf, as the other Omarchy system
drop-ins are, so an administrator's override.conf sorts after it and
wins. It is installed and removed wherever the hook is (setup, teardown,
apply-lock, migration), and apply-lock-test.sh redirects it into its
scratch tree alongside the hook.
The hook's comments now say what actually happens on a locked resume --
Omarchy locks before every suspend and the lock screen opens a verify at
once, so the restart is real, not a no-op -- and name the upstream
defects this works around, fprintd#173 and fprintd#216, so the hook and
the drop-in can be retired when upstream fixes them.
Measured by MaxMad75 on an X390 Yoga (S3): 2 of 10 fprintd stops rode out
the timeout to SIGKILL; the 3s cap verified with systemctl show.
Co-authored-by: Omabot <omabot@omarchy.org>
Co-authored-by: MaxMad75 <44462964+MaxMad75@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
Claude-Session: https://claude.ai/code/session_0168egYTXrVBVg16ugszGzQt
* Close the status-check and start-failure exits through settle
Two paths left the fingerprint loop stuck or misreporting. A mid-lock status check that found fingerprint unconfigured aborted the PAM context directly; abort() delivers no signal, so fingerprintAuthenticating stayed true and every later attempt and nudge returned on it until the password unlock. And a fingerprintPam.start() that fails synchronously means the PAM file is gone -- a configuration problem, not a reader miss -- yet it fed the reader streak and reported "Fingerprint reader unavailable".
Route the abort through settleFingerprintAttempt like the reach timeout does, drop the pending retry with it, and on a start failure re-check the configuration so the icon disappears instead; a pending retry owns the next attempt when a status check comes back configured.
Claude-Session: https://claude.ai/code/session_0168egYTXrVBVg16ugszGzQt
* Pace fingerprint nudges by the pending tier and the cap's idle stretch
The nudge cooldown was a flat 2s, shorter than every backoff step, so a
user moving the mouse at a wedged reader collapsed each wait to 2s --
thirty claims a minute against the cap's 1.5 -- and each claim re-armed
fprintd's 30s idle timer, so the hook-less recovery the cap exists for
never happened while anyone was present.
Grow the cooldown with the pending wait, so presence collapses each
backed-off wait once and repeat nudges are paced by the tier. At the cap
the wait itself is the cure -- it is what lets fprintd idle out and drop
a wedged claim -- so there the idle stretch is measured from the last
settle, not the last nudge: a nudged attempt that hung until the reach
timeout would otherwise eat most of the window, and under continuous
input fprintd would never be left alone long enough to exit.
Wall-clock steps are handled in both directions: a clock stepped back
past the last nudge does not hold a fresh nudge back, and one stepped
back past the last settle counts as no idle time at the cap rather than
as enough. The retry test drives continuous input against attempts that
hang to the reach bound and checks the gap fprintd is left.
Claude-Session: https://claude.ai/code/session_0168egYTXrVBVg16ugszGzQt
* Give a slow fingerprint claim time to land before aborting it
The reach bound aborted any attempt that had not prompted within 5s by
SIGKILLing the PAM child mid-Claim. A reader whose device open takes
longer than that (out-of-tree drivers, and any reader right after the
resume hook forces a re-open) could then never prompt: each kill left
fprintd tearing the claim down until the open finished, the 1s retry hit
"already claimed", and three misses later the reader was reported
unavailable for good. Raise the bound to 20s, under GDBus's 25s Claim
timeout and pam_fprintd's 30s verify timeout (whose "Verification timed
out" is a non-error message that would read as reached), and name the
hazard the bound actually covers: a daemon restarted under the verify
fails the attempt promptly, a stuck device open does not.
Claude-Session: https://claude.ai/code/session_0168egYTXrVBVg16ugszGzQt
* Detect resume and hold the streak through the restart window
Monotonic timers pause across suspend, so a backed-off wait armed before
the sleep picked up mid-count afterwards: with the streak at the cap the
"Fingerprint reader unavailable" notice stayed up for the remaining wait
after the resume hook had already freed the reader, and misses collected
around the suspend edge carried across it, so a healthy reader could
cross the notice threshold in the first seconds after waking. With the
restart enqueued off the thaw, the loop's first attempts after a wake can
also land on the old daemon while it is being stopped -- up to ~3s when
it ignores SIGTERM -- and three of those would show the notice for a
reader that was merely being restarted underneath.
Notice a resume from any of three signals -- a sleep watch ticking the
wall clock for the whole lock, a retry that fired late, or an unreached
attempt whose settle finds the watch's last tick far in the past (so a
suspend shorter than the reach bound is caught before the tick itself
gets a chance to) -- and open a grace window: the stale streak is
dropped, a pending wait retries the fresh daemon at once, and misses
inside the window hold the streak at the first tier without ever counting
toward the notice. Detection is idempotent within the window, since more
than one timer can notice the same resume.
Pinned by MaxMad75's reading: the window is armed by the resume, not by
the first miss. Verified on his X390 (S3, frozen sessions): six lid-close
cycles, fingerprint-resume at +15ms, streak held, notice never fired.
Co-authored-by: MaxMad75 <44462964+MaxMad75@users.noreply.github.com>
Claude-Session: https://claude.ai/code/session_0168egYTXrVBVg16ugszGzQt
* Only let a definitive probe change whether fingerprint is configured
The status probe collapsed every fprintd-list result into yes or no, so
an unreachable fprintd -- restarting under the resume hook, or failing a
D-Bus activation mid-resume -- read as "not configured": the icon
vanished, the retry loop and the sleep watch stopped, and nothing asked
again for the rest of the lock. One transient miss killed fingerprint
until the next lock, with the password as the only clue. MaxMad75 hit it
on hardware in run 6 of the X390 series; osborng filed the stock repro as
#9453 (mask fprintd, lock, unmask -- fingerprint never returns).
Classify the probe's output instead: an enrolled-print row is yes,
fprintd's explicit no-prints answer (or a missing PAM file or binary) is
no, and anything else is unknown -- the probe could not tell, so nothing
changes and it is retried on the attempt-retry pacing. The unavailable
notice, backoff, and resume detection all sit downstream of this flag;
now only an answer that actually means something can clear it.
Fixes#9453.
Claude-Session: https://claude.ai/code/session_0168egYTXrVBVg16ugszGzQt
* Log the fingerprint loop's misses, notice, and recovery as lock events
The reach timeout, an unreached settle, the streak crossing into the
notice, a resume restart, and the recovery all changed lock state without
touching logEvent, so a report of "Fingerprint reader unavailable" left
no omarchy lock line to line up with suspend and resume timestamps in
omarchy-debug-idle output. Log those transitions; reached attempts are
the steady state and stay quiet. A match that unlocks after a run of
misses is the recovery too -- the unlock resets the streak without
settling, so it logs fingerprint-recovered there as well.
Claude-Session: https://claude.ai/code/session_0168egYTXrVBVg16ugszGzQt
* Abort an attempt stranded in flight when a resume is detected
A verify that survived into the suspend still prompted comes back to a
daemon the resume hook has already replaced, and the loop's resume
handling deliberately left it alone: the reach timer stopped at the
prompt, so nothing bounded it but pam_fprintd's own ~25s timeout, and
until that ran out the icon invited touches that could not work. Most
visible where user sessions are not frozen across sleep and the lock
races the hook.
Abort the stranded session when the resume is detected and route it
through settle: it lands inside the grace window, so the kill never
counts toward the notice, and the settle arms the fast retry against the
fresh daemon itself.
Suggested by sliekens in review.
Claude-Session: https://claude.ai/code/session_0168egYTXrVBVg16ugszGzQt
* Simplify fingerprint recovery and consolidate enrollment checks
Use the enrolled-entry matcher from #9551 while retaining the lock's tri-state probe recovery and the privileged /usr/bin/fprintd-list call. Unknown enrollment probes must preserve existing PAM and resume recovery rather than deleting the machinery needed to recover. Preserve administrator-owned unnumbered timeout files during migration.
Remove presence-driven retry overrides and their cooldown, clock, and idle-window state: resume has its own fast recovery path, while other errors can follow the bounded automatic backoff. Let the existing sleep watcher detect resume instead of also tracking the age of each retry. Setup now uses apply-lock so PAM and recovery installation have one implementation. Keep the restart, stop bound, unreachable-attempt pacing, unavailable feedback, reach watchdog, and probe rechecks because each handles a distinct failure.
Co-Authored-By: Karl Ahlin <kalle.ahlin@gmail.com>
Co-Authored-By: Codex Medium <noreply@openai.com>
* Preserve failed-enrollment coverage in the setup fixture
The successful-enrollment fixture accepts PAM commands, so failure checks must explicitly reject those commands instead of relying on an unexpected-command error. Log both sed and tee and stub apply-lock for every case so premature authentication setup is detected without reaching live PAM files.
Co-Authored-By: Codex Medium <noreply@openai.com>
* Complete fingerprint recovery and setup reporting
Back off immediate device errors after the verification prompt as well as failed claims, while retaining fast retries for mismatches and normal scan timeouts. Measure from the prompt so a slow claim cannot hide a fast failure. Paced user activity retries preserve the daemon idle window required to clear a wedged claim. Initial probe outages remain visible without inventing enrollment, and setup cannot claim lock-screen success when the PAM configuration was not installed. Exercise the real QML service rather than a copy of its state machine.
Co-Authored-By: GPT-6 <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 Medium <noreply@anthropic.com>
* Avoid competing fingerprint probes and partial setup
Known enrollment is recovered by the PAM retry loop, so failed status probes must not raise a false unavailable notice or interrupt its daemon idle window. Initial unknown enrollment still gets paced probes. Install recovery files before enabling fingerprint PAM so a missing source cannot leave a new partial configuration.
Co-Authored-By: GPT-6 <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 Medium <noreply@anthropic.com>
* Keep the fingerprint error clock at the first prompt
pam_fprintd also sends Verification timed out as an informational message. Updating the prompt timestamp on that message made a normal full scan window look like an immediate device error and caused unnecessary backoff. Record the first prompt of each PAM attempt so later status messages cannot move the error window.
Co-Authored-By: GPT-6 <noreply@openai.com>
---------
Co-authored-by: Omabot <omabot@omarchy.org>
Co-authored-by: MaxMad75 <44462964+MaxMad75@users.noreply.github.com>
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: Karl Ahlin <kalle.ahlin@gmail.com>
Co-authored-by: Omarchy Bot <omarchybot@users.noreply.github.com>
* Make application transparency opt-in
* Use the transparency helper for Kanagawa
* Address transparency review findings
* Toggle transparency for opaque windows too
The tuning host loaded module-rt with default arguments, so it asked for
realtime through xdg-desktop-portal. A portal started before rtkit was
installed reports a realtime budget of zero. module-rt applies that as a
hard RLIMIT_RTTIME of 0, RTKit still makes the data thread SCHED_RR, and
the kernel kills the host with SIGKILL as soon as audio plays; systemd
restarts it every two seconds and the speakers stay silent until the next
login.
Set rtportal.enabled = false, as the stock pipewire.conf does. The host is
a user service in the same session as the PipeWire daemon and has no
reason to go through the portal.
The host config is only copied by `omarchy-audio-tuning on`, so a
migration replaces an installed copy that is still Omarchy's own with the
old setting, and restarts the host if it runs. It does not call `on`,
which would overwrite a tuning graph another tool wrote under the same
name.
Co-authored-by: Michael de By <contact@michaeldeby.nl>
* Resolve effect_input/effect_output audio filter pairs
User filter chains can name their input and output nodes differently, so the existing input-name prefix check cannot find their downstream sink. Match effect_output.<suffix> exactly for effect_input.<suffix>, avoiding similarly named filters while retaining prefix-compatible custom tunings and the existing EasyEffects fallbacks.
Generated by GPT-6 in Codex. Reviewed by Claude Opus 5.5 Medium.
Co-Authored-By: Claude Opus 5.5 Medium <noreply@anthropic.com>
* Prefer an exact filter output over legacy prefix matches
An earlier stream for a similarly named filter can satisfy the legacy prefix check and select the wrong device before the exact paired output is read. Keep the first prefix candidate as a fallback for effect_input sinks and finish searching for the exact effect_output pair. Preserve first-match behavior for all other sink names.
Generated by GPT-6 in Codex. Reviewed by Claude Opus 5.5 Medium.
Co-Authored-By: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
---------
Co-authored-by: Omarchy Bot <317366263+omarchybot@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 Medium <noreply@anthropic.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Text labels centered their whole line box, which keeps room for
descenders, so the clock and workspace numbers sat a pixel above the
icons beside them. Center single-line labels on the figure height
instead. The menu logo comes from an icon font and opts out.
The Elsewhen globe also took the wide icon slot while its neighbors in
the center cluster use the status slot, so it drew extra space on both
sides. Give it the status slot like the weather widget.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Apply agent account selection to CLI subprocesses
* Fix account dispatch edge cases and remove redundant shell wrappers
* Use the current account when launching a new agent session
* Instantiate only the current orientation's indicator tree
Indicators.qml built both the horizontal Row and the vertical Column and
toggled them with `visible`, so every indicator existed twice per bar,
and so did every process an indicator spawns: Dictation.qml ran two
`voxtype status --follow` per monitor. On a six-monitor bar that is 72
indicator instances and twelve followers for six visible icons, and each
instance registers a click target and re-syncs the active-indicator model
as its state resolves at startup.
A Loader now instantiates the tree that matches `root.vertical`. Each tree
is wrapped in an Item that keeps the stock explicit implicit-size
expressions, so the root's size still follows the blocks synchronously; a
bare positioner only updates its implicit size on polish, which the
indicator contract test's center-hover check catches.
Measured on a six-monitor, 23-widget bar (three runs each, `omarchy
restart shell`): time from "Configuration Loaded" to "polkit agent
registered" 19.8-20.3s -> 15.5-15.7s, quickshell CPU 29-30s -> 24-25s,
voxtype followers 12 -> 6.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Coalesce plugin API resyncs and key bar object ownership by target
Every WidgetButton registers itself as a bar click target when it is
created. registerClickTarget replaced the clickTargets array, the change
handler ran syncAllPluginBarApiObjects() inline, and that walked every
plugin API times every click target times a linear scan of
pluginObjectOwners in pluginObjectRecord. Startup is a few hundred
registrations, so the cost is quadratic in bar size and multiplied by the
number of monitors: a six-monitor, 23-widget bar spent 16-20 seconds of
pegged QML thread before it was populated, and an 8-second qmlprofiler
capture showed 1.36 million pluginOwnsBarObject calls and 46-71ms per
registration.
- pluginObjectOwners is a Map keyed by target, so pluginObjectRecord,
markPluginObject, unmarkPluginObject and releasePluginObjects are O(1)
per object. Nothing outside Bar.qml read the array.
- The activePopout, clickTargets and layoutConfig change handlers schedule
one resync per event-loop turn through Qt.callLater, the way
onModuleSlotsChanged already defers prunePluginBarApis. bindPluginBarApi
still syncs a brand-new API inline, and requestPluginPopout and
releasePluginPopout sync the owning API inline, so a plugin never reads
a stale API on its own actions.
- A flush serialises the layout once and hands each API its own parsed
copy instead of deep-copying it once per API per sync.
- ModuleSlot's cursorShape read clickTargets for every slot on every
monitor (26,700 evaluations per start). It is now gated on the slot's
HoverHandler, which is the only time the cursor is over it.
Measured on the same bar, launching the shell from a checkout with this
and the indicators change (two runs): "Configuration Loaded" to "polkit
agent registered" 19.8-20.3s -> 0.70s, bar populated 1.7s after launch
(from ~30s), quickshell CPU 29-30s -> 2.0s.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Wait for the old shell to exit before restarting it
omarchy-restart-shell stopped the running shell with `quickshell kill`
under a five-second timeout and launched the replacement as soon as the
loop ended. A six-monitor bar takes 5.4-6.0 seconds to tear down (every
widget button unregisters its click target on destruction, and each
unregistration re-synced every plugin API), so the client timed out while
the shell was still exiting, the fresh instance's no-duplicate check saw
the dying one and quit, and the user was left with no bar and "Omarchy
shell did not become ready after restart".
Give the kill client thirty seconds, then wait, bounded, until
`quickshell list` shows no instance of the session config before
launching. The readiness check also waits on a sixty-second deadline
instead of twenty attempts: a large bar answers ping only after its
plugins have loaded, which on the stock bar was well past the old
twelve-second window.
Verified three consecutive restarts against the stock shell on the
six-monitor machine: each returned 0 in about six seconds with exactly
one instance and no "already running" in the journal.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Say what the bar's ownership Map actually saves
Qt's V4 Map (ESTable::get) finds a key by scanning its keys, so ownership lookups are not O(1). The win is that a registration no longer copies the owner array and rescans it in QML.
Co-Authored-By: Codex Medium <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Omarchybot <317366263+omarchybot@users.noreply.github.com>
Co-authored-by: Codex Medium <noreply@openai.com>
User setup recreates installer-seeded files, so presenting a full rerun as a refresh or an after-login recovery step implies that it safely preserves an existing account. Remove that advice from first-boot provisioning and both user-setup entry points. Setup failures still report the log path, and provisioning behaviour is unchanged.
Co-authored-by: Omarchybot <317366263+omarchybot@users.noreply.github.com>
Co-authored-by: GPT-6 <noreply@openai.com>
* Apply the system keyboard layout to the SDDM greeter
* Extract shared assert_resolved_input helper
* Make the missing vconsole.conf tests actually open nothing
The stub only returned nil for an unset OMARCHY_VCONSOLE, but bash passes it
through as "", so both fallback cases were reading an empty temp file instead
of exercising the failed open.
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>
* Keep the Wi-Fi icon steady on OWE transition-mode networks
An OWE transition-mode network pairs an open SSID with a hidden "_owetm_"
twin on the same BSSID. Between scans NetworkManager reports the in-use
access point under the hidden SSID and drops the active profile from the
device's AvailableConnections, which is the only place Quickshell builds
known networks from. No listed network is then connected, so the bar fell
back to the disconnected icon until the next scan brought the open SSID
back, flickering on and off while the link stayed up.
Fall back to the Wi-Fi device's own connected state when no listed network
is connected, and read the in-use access point's strength from nmcli
(without a rescan) while the connected network has none of its own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Discard stale in-use AP reads and key Wi-Fi checks on the device
Bump a generation whenever the cached in-use access point strength stops
describing the link (leaving Wi-Fi or a device change), and drop any nmcli
read started before it, re-reading immediately instead of a full interval
later.
Key Wi-Fi connectivity checks on the device rather than the SSID, so the
listed network coming and going with each scan on an OWE transition-mode
network no longer schedules a check. A real network switch still passes
through "disconnected".
Add a QML fixture that runs the panel against a mocked device through the
scan churn, a mid-read disconnect, and a final disconnect.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A pending migration must rebuild after restoring its missing display configuration even when another user previously completed the machine-wide rebuild. Remove the fixed marker before writing the drop-in so failed writes and builds remain retryable. Fresh installation already rebuilds after hardware setup through the ISO finalizer.
Co-Authored-By: GPT-6.1-Sol XHigh <noreply@openai.com>
Co-Authored-By: Greptile <165735046+greptile-apps[bot]@users.noreply.github.com>
Disable PSR2 selective fetch and Panel Replay on DX13260 Panther Lake through a Limine drop-in. Apply it during hardware setup and an update migration, rebuild once per machine, and request reboot until both parameters are booted. Reject diagnosed Limine build errors even when the wrapper returns success so interrupted repairs stay pending.
Co-Authored-By: GPT-6.1-Sol XHigh <noreply@openai.com>
* Pin root= before the packages that can drop it
limine-entry-tool falls back to /proc/cmdline for root= only while nothing
appends to KERNEL_CMDLINE. Installing omarchy-settings lands a drop-in that
appends with +=, switching that fallback off, and a kernel bump in the same
transaction then bakes a UKI with no root= at all. preserve_kernel_cmdline_root
repaired that afterwards, so a completed upgrade booted — but the machine was
unbootable for the seconds in between, and an upgrade interrupted there left it
in an emergency shell.
Pinning cannot wait until after the packages land, and the old guard could not
be moved earlier as it was: it asked the tool for its effective cmdline, which
still resolves root= through the fallback right up until the drop-in arrives,
so it reported healthy on exactly the machines about to break. Ask the config
layers whether root= is stated explicitly instead, for the default profile
alone, and pin before the package transaction. Verification stays after it,
since that is what rebuilds the UKIs.
The pin no longer gates on limine-mkinitcpio being present, since it now runs
before the transaction that can install it, and a machine still missing it is
exactly one that needs pinning first.
Verified in a VM upgrading a 3.8.0 install, sampling the UKI every 2s across the
upgrade: a legacy install without the pin lost root= for ~10s, and booting that
state landed in "Failed to mount '' on real root". With this change the same
upgrade never loses it, and an install that already pins root= is untouched.
Closes#6894
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Merge the limine config layers when checking for a root= pin, and re-pin after the transaction
The pin check returned on any line that mentioned root= under KERNEL_CMDLINE[default]. limine-entry-tool merges its layers in order, /usr/share drop-ins, /etc/limine-entry-tool.conf, /etc drop-ins, then /etc/default/limine, where = replaces and += appends, so a later layer's = could remove a pin the check still counted, and a quote before root= let systemd.setenv="root=..." read as one. Both skip the pin, which is the direction that bricks the next boot. The check now merges the layers the same way, strips only the outer double quotes the tool strips, drops quoted values the kernel does not split, and looks for root= as a parameter of its own. It was checked against limine-entry-tool --get-cmdline on a worker for every fixture in the test.
Installing limine-mkinitcpio-hook runs limine-install, so a machine with no /boot/limine.conf when the pin runs ahead of the transaction can have one after it, and verification only warned about the UKI it found without root=. Verification now runs the pin for any machine the first call skipped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex Medium <noreply@openai.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: omarchybot <omarchybot@users.noreply.github.com>
Co-authored-by: Codex Medium <noreply@openai.com>
Hyprland lets a headless output take any name (`hyprctl output create headless <name>`), so a name with a quote broke out of the Lua string and the toggle failed for that output. Escape backslashes and quotes instead of asserting a character set the names do not actually follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex Medium <noreply@openai.com>
docs/testing.md asks every shell test to start with set -euo pipefail and source base-test.sh through BASH_SOURCE; without it a failed mktemp leaves test_home empty and reset_scenario goes on to rm -rf /data.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EasyEffects plays into Bluetooth headphones through the same direct port links as into a card, but only alsa_output targets were followed, so with headphones on the keys fell back to the settings file -- which can still name the speakers -- or to easyeffects_sink. Match bluez_output sinks as well, and compare the configured device name literally rather than as a regex.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Read Codex app-server replies from the raw fd (#13703)
* Resolve Codex through mise which instead of running the lazy launcher (#13109)
* Skip the Codex app-server probe when there are no credentials (#13106)
Adapted: credentials are checked in the home being probed rather than in
the CODEX_HOME environment variable, since each registered account is
probed in its own home, so a signed-out secondary account isn't hidden
behind the primary's login. A home without credentials reports "Waiting
for auth" like any other signed-out home. The credentials store setting is
read with tomllib, so a single-quoted value counts too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Show the Codex CLI's own error when its app-server dies (#8977)
Detect an app-server that exits or stops answering, and report the end of
its stderr instead of a bare RPC method name. Rebased onto the raw-fd
reply reader; the switch from "-a on-request" to "-a never" is left out,
keeping the current approval flags.
* Count pi sessions when HOME is a git checkout (#13209)
* Count only OpenAI-backed native sessions as Codex usage (#12032)
* Deduplicate Pi usage across forked sessions (#8602)
* Skip unchanged native Codex token snapshots (#10531)
* Count omp and pi profile sessions in the agent usage collectors (#9546)
`omp --profile=<name>` (and pi's equivalent) relocates the whole agent
tree under <base>/profiles/<name>/. The Claude and Codex collectors only
ever scanned <base>/agent/sessions, so a subscription driven entirely
through a profile was invisible to the agents panel: no tokens by day, no
tokens by model, no prompt or session counts.
Discover the profile roots alongside the default one. Sessions are keyed
by file path, so a profile adds sessions instead of double-counting the
default root, and a missing or unreadable profiles directory leaves the
existing behavior untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014zFbJcDEEpV5BAmsH6kAB3
* Skip unrelated Codex session lines before JSON parsing (#12803)
Adapted: session_meta lines also pass the pre-filter, since the provider
filter from #12032 reads them to skip rollouts served by a non-OpenAI
provider.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Read only the Codex session files that changed since the last scan (#12595)
Native Codex rollouts keep per-file totals between runs, replayed while a
file's mtime and size are unchanged. Rebased onto the session_meta
provider filter, snapshot dedup, and line pre-filter, which now live in
the per-file reader. pi and omp sessions are left out of the per-file
cache: a forked pi session repeats its parent's messages, so they are
deduplicated across the whole tree on every scan.
* Count streamed Claude messages by their highest-output usage line (#10606)
Claude Code writes a streamed assistant response as several transcript
lines that share one message id, one per content block. Each line
carries a usage object. The first line's output_tokens is a placeholder,
often 1, and the last line has the real count. Input and cache fields
usually match across the lines.
The scanner dedupes by message id and keeps the first line it sees, so
it under-counts output tokens. On a machine with 2,577 transcripts it
reported 39.0M output tokens against 60.1M used, a 35% shortfall. Input
and both cache fields differed by under 0.01%.
Keep the line with the highest output count, with the last one scanned
winning a tie. The whole line is kept because a response can fall back
to another model mid-stream. Those lines are separate snapshots with
different cache figures and a different model, and taking a maximum per
field across them over-counts cache tokens and credits the wrong model.
The zero-usage check now runs before dedup, so a zero-usage first line
no longer claims a message id and hides a later line with real usage.
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: GPT-6 Astra <noreply@openai.com>
* Index Claude transcripts so the agents refresh reads only what was appended (#8313)
omarchy-agent-usage-claude re-parsed every line of every transcript under
~/.claude/projects on each refresh: no mtime cutoff, no memory of the last
pass. The agents widget is on by default and ticks every 15 minutes, so the
cost grew for the life of the machine. After one month here that was 803
files, 640 MB, 127k lines and 57k JSON parses per tick, about 1 core-second,
pushed through the page cache every quarter hour forever.
Keep a per-file index next to the scan cache: the unique usage records
already parsed out of each transcript and the byte offset they end at. A
file whose size and mtime match is not opened; a file that grew is read
from the stored offset; a file that shrank or was rewritten is read from
the start. --force drops the index and rescans from scratch.
The summary is built from the indexed records in the same directory order
the walk always used. That matters: when a resumed session carries earlier
messages, the same message id appears in two files with different usage,
and the first file visited wins. 91 ids differed on this machine; sorting
the walk moved one model's output total by 25k tokens. Output is now
byte-identical to the previous scan on a frozen copy of the corpus, cold,
warm, and after an append.
Warm refresh: 1.0 s -> 0.10 s of CPU, of which the scan itself is 70 ms;
the index for this corpus is 2.9 MB.
Adapted:
- Rebased onto #10606: the highest-output rule for streamed messages now
lives where the index parses records, and decides between files too.
- The index records the timezone it was written in, and a change rereads
every transcript, since its records hold local days.
- A file only counts as appended to when its inode and the hash of what
was already read still match, so a transcript replaced by a larger one,
or rewritten in place, is read from the start.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Label a Claude Team seat by its subscription, not its rate-limit tier (#11109)
The collector built the plan label from the OAuth rateLimitTier first, so a
Team premium seat, which runs on default_claude_max_5x, showed in the agents
panel as "Max 5x". Lead with subscriptionType and keep the multiplier as its
qualifier: Max still reads "Max 5x", a Team seat reads "Team 5x".
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Label the Claude plan from the profile the CLI refreshes (#7225)
Adapted: the profile is found the same way current_account_id() finds it,
now shared as profile_path(): ~/.claude.json for the default home, the
home's own .claude.json otherwise. The original fell back to ~/.claude.json
for any home without CLAUDE_CONFIG_DIR set, so a secondary account read the
primary's tier. The profile's tier also keeps the subscription in the label,
so a Team seat stays "Team" (#11109).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Call a lapsed Claude access token paused, not signed out (#8093)
* Refresh Claude usage after the clock moves backwards (#9956)
* Bound unreadable Claude transcript warnings (#12414)
* Count Claude usage from opencode v2 sessions (#13894)
* Reload agent usage records when an inotify watch fails to rearm (#10067)
* Reload agent usage records after each update run instead of on a timer
Rather than #10067's two-minute timer per record, reload every record when
the omarchy-agent-usage-update process exits, the moment its files can have
been replaced. A reload that finds a file unchanged keeps its record, so the
panel isn't stirred up by identical data. The grep test now runs the QML
functions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Show the agent status when the trouble line has no help text (#8497)
* Clear stale agent login guidance after a successful probe (#8892)
* Clear the Grok login hint after a successful probe
#8892 cleared the default login hint after a successful probe in the
Claude and Codex collectors; Grok's collector had the same stale hint.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Read Fireworks credentials from pi's auth.json (#7455)
The Fireworks collector skipped pi, Omarchy's default agent, when
walking its credential ladder, so a machine signed in to Fireworks only
through pi (/login fireworks) never showed the tab. Insert the key pi
stores in $PI_CODING_AGENT_DIR/auth.json (default ~/.pi/agent) between
the firectl auth.ini and the opencode fallback.
pi keys can be literals, $ENV_VAR/${ENV_VAR} references, or !command
shell lookups. The collector resolves the first two; command lookups
stay pi-only and are skipped rather than sent to the API verbatim.
* Call a lapsed Grok access token paused, not signed out
Grok's access token lives six hours and Grok mints a new one from its
refresh token whenever it starts, so a lapsed one is routine. Reporting
it as an expired sign-in made the panel offer Sign-in required several
times a day, sending people through grok login for nothing. With a
refresh token present it now reads as paused, like Claude's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep showing Grok's last limits while it sits idle
While Grok hasn't run, nothing on the machine has spent its allowance,
so with a refresh token on hand the last numbers still stand: they show
as current rather than dimmed under a status line. A weekly window that
reset in the meantime starts over at 0%, a whole number of weeks on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Ask for a Grok sign-in once its refresh token is past 30 days
A refresh token older than Grok's 30-day sign-in can't renew anything,
so the panel offers Sign-in required again instead of showing the last
limits as current. With nothing cached yet it says to start Grok, rather
than showing an empty section without a word.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Check both ends of what the Claude index read before resuming a transcript
A transcript rewritten in place could grow and change only after its
first kilobytes, and the index took it for an append. It now compares
the last kilobytes before the resume point too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Simplify the agent usage collectors
- Codex: pass the forced-scan choice down instead of a module global, make
the per-file reader's cache arguments required, shrink the cache record
check, and drop guards for shapes that can't occur: an empty launcher
path, realpath raising, mise itself being a lazy launcher, multi-line
`mise which` output, and probing without a temp file for stderr.
- Claude: decide an append by the digest of both ends of what was read
alone; the inode and mtime checks it made redundant are gone.
- Snapshot: the device id falls back to the hostname, which always exists.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Share fixture setup in the agent usage scanner tests
Every fixture home lives under one scratch directory with a single cleanup
trap, instead of a trap rewritten with a longer list for each new home, and
the Codex test builds its signed-in homes with one helper.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Treat a replaced Claude transcript as new even when its ends match
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Probe Codex without its error text when there's no temporary space
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: tossbaws <17258053+tossbaws@users.noreply.github.com>
Co-authored-by: surim0n <suritech@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: anonwurcod <anonwurcod@proton.me>
Co-authored-by: Kevin Rajan <7121943+kvnloo@users.noreply.github.com>
Co-authored-by: Nate Ashby <nate.ashby11@gmail.com>
Co-authored-by: Aris Gysel <aris.gysel@me.com>
Co-authored-by: Brams <76213579+Brams-s@users.noreply.github.com>
Co-authored-by: This_Is_NPC <gabrielfollone27@gmail.com>
Co-authored-by: sanjyay <102979855+sanjyay@users.noreply.github.com>
Co-authored-by: PapistProtocol <12738904+PapistProtocol@users.noreply.github.com>
Co-authored-by: steez <stevedimakos97@gmail.com>
Co-authored-by: GPT-6 Astra <noreply@openai.com>
Co-authored-by: Ryan Yogan <ryanyogan@gmail.com>
Co-authored-by: Oli Denton <41393837+omdenton@users.noreply.github.com>
Co-authored-by: Igor Kramar <i@ikramar.ru>
Co-authored-by: Martin Eidensten <martin@meibe.se>
Co-authored-by: Romain Perron <rdj.perron@gmail.com>
Co-authored-by: Omarchy Contributor <contributor@users.noreply.github.com>
Co-authored-by: manuaudio <manu@arimaka.com>
Co-authored-by: Tyler South <tsouth2@gmail.com>
Co-authored-by: whathek <Hek846@users.noreply.github.com>
Co-authored-by: Ty Richards <me@tyrichards.com>
Quote the marker path as a Bash literal before escaping it for sed, so TMPDIR metacharacters survive both fixture copying and script evaluation. Rerun the firmware regression under a special-character TMPDIR while preserving the original PATH for real pacman resolution.
Co-Authored-By: GPT-6.1-Sol <noreply@openai.com>
Co-Authored-By: Greptile <165735046+greptile-apps[bot]@users.noreply.github.com>
A caller-controlled marker path would let the privileged install command replace an unrelated file. Keep the production path fixed and redirect that literal only in isolated test copies. Cover hostile environment overrides without touching system files.
Co-Authored-By: GPT-6.1-Sol <noreply@openai.com>
Co-Authored-By: Greptile <165735046+greptile-apps[bot]@users.noreply.github.com>
Explicitly select the Omarchy Cirrus firmware when the installed package predates the 1028:0e54 aliases. Apply it during hardware setup and through an update migration, and request a reboot for each user until the firmware can load. Require the same minimum version in the offline package list so newer Arch snapshots keep their genuine firmware.
Co-Authored-By: GPT-6.1-Sol <noreply@openai.com>
* Let the agents in the panel be put in any order
Drag an agent by its mark to move its section; the header it will land
on lights up, and the move happens on release. Each agent's header is
now a keyboard stop with its own highlight, and Ctrl+Up/Down moves the
agent the cursor is in. The order is kept in agents/order.json beside
the usage records. The key catcher turns Ctrl+Up/Down into a reorder
only for panels that opt in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Light only an agent's mark when the cursor or a drag is on it
The mark is the handle the agent moves by, so the keyboard cursor and
the drop spot while dragging box it alone rather than the header line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep a lit agent mark's box from being clipped at the panel's edge
The box overhangs the content's left edge, which the scrolling area
clipped. The scrolling area now reaches a little into the panel's
padding with the content shifted back, so nothing moves and the box
draws whole.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Reuse a Grok session scan only on the day it was made
A limits-only refresh just after midnight reused a scan from the evening
before, which counted yesterday's sessions as today's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep Ctrl+Up/Down from moving an agent when the cursor is outside one
The hero's buttons and the starter tiles carry indices too, and the
lookup read them as accounts, so with several accounts Ctrl+Up/Down on
one of them moved an unrelated agent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Report a failed mise update even after the Grok upkeep runs
The Grok block ran after `mise up` and its last command set the script's
status, so a failed tool update could read as a success to callers that
warn about it. The update's own status is now the script's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Unpack a Grok update into ~/.grok whatever GROK_HOME says
The upkeep replaces ~/.grok's link, but the npm launcher unpacks into
GROK_HOME when it's set, so with a custom home the link never came back
at the new release and the old one was restored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Hold off reordering agents while an account name is being edited
Ctrl+Up/Down reached the key catcher during an inline rename, and moving
the agent rebuilt its section, dropping the unfinished name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Install Grok through mise's first-party package
The npm launcher keeps an old ~/.grok/bin binary because mise skips its
postinstall. Use mise's grok tool, and drop the npm tool so its shim
does not stay ahead of the stub.
* Drop the npm Grok workarounds now that mise installs Grok itself
With Grok installed through mise's first-party package, the CLI is the
binary mise manages, so the update upkeep that repointed ~/.grok/bin and
the shared bin directory for added Grok accounts have nothing left to do.
omarchy-update-mise is back to running mise up and nothing else, and
adding a first Grok account installs the same package.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Count Grok's tokens and prompts from its usage ledger
Each Grok session keeps a usage.json, the ledger `grok usage` prints, with
every finished turn's end time and tokens by model. The collector now
reads it for tokens today, by day for the last week, and by model, with
cached input kept apart, and counts today's prompts by the turns that
ended today. Found in #12352's research.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Count cache writes in Grok's daily token totals
Grok's totalTokens leaves cache writes out while the per-model buckets
count them, so a turn with cache writes added less to its day than to
its model. The day's total is now the sum of those same buckets.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Remove the npm launcher's old Grok binaries when moving to mise's Grok
Omarchy's npm wrapper ran the binary the launcher unpacked into
~/.grok/bin, where x.ai's installer also puts a copy with a PATH entry
ahead of mise. Once the wrapper is replaced, a binary left there would
keep shadowing the mise tool, so the migration removes it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Jesse Miller <jmiller@jmiller.com>
The layout check skips comments, but the already-fixed check matched `include system-auth` anywhere on a line, so a comment such as `# auth include system-auth` made the migration skip a stack that still listed bare pam_unix, and it was marked complete with the lockout still missing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`sudo tee` truncates the live PAM file before writing it, so a migration interrupted in between left /etc/pam.d/polkit-1 empty; the next run then failed the layout check, exited 0, and was marked complete with polkit authentication broken. sed -i writes a temporary file and renames it into place, as the setup commands already do, so the original stays intact until the rewrite is whole and a failed sed needs no restore.
The test's broken-write stub now empties the file instead of discarding the write, so the restore assertion fails if the restore is removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex Medium <noreply@openai.com>
* Let Grok updates through mise take effect
Grok's npm launcher runs ~/.grok/bin/grok whenever it exists, and the
install script that repoints it at a new release doesn't run under mise,
so every machine kept running the release it first unpacked. Updating
mise tools now drops a link to an older release and the old binary, and
lets the launcher unpack the installed one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep several Grok accounts, the way Claude and Codex do
Grok honors GROK_HOME, so an added Grok account gets its own home holding
only its login and settings cache, with the CLI binary, sessions, skills,
plugins, memory, and config linked back to ~/.grok. The account commands,
the add flow (a second account signs in through a private window), the
launcher, and a grok shell function all take it like the others, and its
identity and plan come from the files Grok writes at login.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Show Grok's plan and credits in the agents panel
A grok collector reads each account's plan from the settings Grok caches
and its credits from the endpoint behind Grok's own /usage view, so Grok
gets a section, per-account limits, and autoswitch like Claude and Codex.
A signed-in agent with a plan now shows before its first numbers, so a
lapsed sign-in has somewhere to say so. Grok's mark joins the assets, and
with every agent able to take another account the add screen no longer
needs to dim one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Document Grok accounts and limits alongside Claude and Codex
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Read Grok's credits the way xAI actually answers
The answer nests under config, names a weekly period with its end, and
as protobuf JSON leaves the usage percentage out while it's zero. The
collector now reads that shape, so a fresh week shows as 0% until it
resets rather than as nothing known.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Count Grok's prompts and sessions from its session summaries
Grok keeps a summary beside each session with when it was last active
and how many prompts it had, so its section and the hero's summary get
today's prompts and sessions and its active days. It records no token
counts there, so none are claimed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Give each Grok limits cache write its own temporary file
Two overlapping collector runs wrote the same cache through one .tmp
path, so one rename could leave the other's failing and its record not
updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Read a single Grok account from GROK_HOME when it's set
The launcher and the CLI honor GROK_HOME, but the collector always read
~/.grok, so a Grok signed in only in a custom home showed nothing. With
one account, the collector now reads the home the CLI would; with
several registered, the primary stays ~/.grok.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drop guesses the real Grok credits answer made unnecessary
The collector carried fallbacks for fields and shapes guessed from the
binary before xAI's actual answer was seen, a numeric timestamp branch
nothing writes, the panel's default for prompt stats, and guards that an
empty sign-in already covers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep the working Grok release until its update is in place
The update dropped the link and older binaries before the new release was
unpacked and ignored a failed unpack, which could leave every Grok
account without a CLI. The old release now stays until the new one is
linked, and its link comes back if it never is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Count only Grok sessions for today, not prompts
A session's summary holds all its prompts and only when it was last
active, so a resumed session put its whole history on today. Today now
counts the sessions active in it; prompts stay an all-time total.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Reuse the Grok session scan on a limits-only refresh
Opening the panel and near-limit checks only need fresh limits, so they
reuse a scan up to 15 minutes old, as the Codex collector does; --force
still rescans.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Plan multiple Claude and Codex accounts with manual or automatic switching
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep several Claude and Codex accounts and start new sessions as the active one
Each added account gets its own home holding only its login, with history,
settings and skills linked back to ~/.claude or ~/.codex so --continue works
across a switch. Accounts are added through the CLI's own login, and cx, cy,
plain claude/codex and omarchy-agent all start as the active account unless
CLAUDE_CONFIG_DIR or CODEX_HOME is already set.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Report limits for every Claude and Codex account in the usage records
Each registered account is probed with its own sign-in and cached on its own,
and a parked account whose sign-in lapsed keeps its last-known numbers marked
stale. The record's top-level limits keep describing the active account.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Switch accounts automatically near a limit, or notify with a one-click switch
After each usage update, an active account at or over its provider's
threshold (95% by default) moves new sessions to the account with the most
headroom in auto mode, or offers that switch as a notification in manual mode.
It never flaps back to an account that just reset, and says once when every
account is over.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Show every subscription account's limits in the agents panel
With several Claude or Codex accounts, the limits become one card per account
with the active one badged. Pick a card with its number and press Enter (or
click Use) to move new sessions to it, press a to add an account, and m to
toggle automatic switching. Limits refresh every minute while an active
account is above 80%.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add Setup > Agent Accounts to list, switch and add Claude and Codex accounts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Document switching between several Claude and Codex subscriptions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Count a rested parked account as available, and pin Main to its own home
A parked Claude account whose windows all reset now reads as 0% instead of
unknown, so switching can pick it. Main's Codex limits come from ~/.codex even
when CODEX_HOME is set, and duplicate logins are checked against who each home
is signed in as now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Key limits caches by subscription and report when an account truly frees up
An added account's limits cache follows its account id, so a new account
reusing a removed one's label never inherits its allowance. The all-accounts
notice now names when an account's blocking windows have all reset, not the
earliest reset of any window.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep account ids clear of routing keywords and refresh the panel after removal
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Share Codex plugins and hooks across accounts, and key Claude caches by current sign-in
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drop the key hint from the Add account button
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sign new agent accounts in through a private browser window
The main browser is almost certainly signed in to the account you already
have, and the login would silently reuse it. Both CLIs open their login page
through $BROWSER, so it now points at omarchy-launch-browser --private.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Let agent account commands default to your default agent's provider
With Claude or Codex as the default agent, the provider can be left out:
omarchy agent account use work, and primary names the primary account.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drop the primary alias, which shadowed an account named Primary
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Replace the auto switch button with a small Notify / Autoswitch toggle
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add accounts from a small + beside the switch toggle
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix the punctuation of the switch toggle's README line
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drop the border around the add account +
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Rename Claude and Codex accounts without moving their homes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Lay out agent accounts on accent rails instead of boxed cards
The active account gets an accent rail and the others a quiet one, each
window is a single compact line, and the switch toggle, add, and Use are text.
Clicking an account's name renames it in place. A window without a reset time
no longer leaves an empty line, and last-known numbers are only red when the
sign-in needs attention.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Read Codex limits without waiting on account/read
Codex 0.158's app-server can leave account/read unanswered, and asking it
first lost the limits whenever it did, leaving the agents panel showing
"Codex limits unavailable". The limits name the plan themselves, so they're
asked first and account/read is only a short fallback when they don't.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Switch agent providers from their marks in the panel header
The row of provider buttons gives way to a small mark per provider in the
hero's corner, the selected one at full strength, with the add account +
beside them in place of the + by the switch toggle and the full-width Add
account button.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep the agents panel's scrollbar off its contents
The panel's content narrows to leave the scrollbar its own strip whenever it
scrolls, and the add account + leads the provider marks instead of sitting at
the very edge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add Claude, Codex, or Grok subscriptions from one Add Account menu
The first account for a provider installs its CLI if needed and signs in to
the CLI's own home through the normal browser; only additional accounts get a
home of their own and a private window. Grok signs in its first account.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Reveal Autoswitch beside Use instead of a Notify / Autoswitch row
The panel's + now opens the Add Account menu for any provider.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Split agent accounts with plain separators instead of rails
ACTIVE already marks the account new sessions use.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Only call Claude limits stale once they're old, and poll near a limit less
Anthropic rate-limits its usage endpoint, and polling two accounts every
minute near a limit got every re-check refused, so both accounts read
"Last known" with numbers a minute old. A refused check of numbers under
15 minutes old now counts as current, older ones say how old they are,
and near-limit polling is every three minutes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Show every agent's limits on one page under an Agents hero
The panel stops switching between one provider at a time and lists every
agent and account with its limits, dropping the tokens-by-day and by-model
charts. The hero carries the agents robot and rotates through what the token
counts add up to: tokens this week and today, the most used model, the
busiest day, and today's prompts and sessions. Middle-clicking the bar icon
refreshes, since there's no provider left to advance to.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Report Codex's free rate-limit resets in its usage record
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Give the agents panel more room, and show Codex's free resets
Limit lines are a notch larger with more space between them, each account's
limits sit a clear step below its name, and sections breathe. Codex has one
limit on Pro, so its section now also says how many free full resets are
waiting and when the next one lapses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Mark Claude's Fable limit on the Weekly meter instead of its own row
A model-scoped allowance on the same clock as a base window is drawn as a
tick on that window's meter and named in the row's tooltip.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Let omarchy-default-agent set the default without launching it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Let the agents panel drive adding an account without a terminal
--check says whether adding one now would be each provider's first sign-in
or an additional account, and --events reports progress as tagged lines
alongside the CLI's own output, notifies with the result, and cleans up the
login and its scratch home when cancelled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add subscriptions and pick the default agent right in the agents panel
The + is a proper accent button, and it swaps the list for a picker of
Claude, Codex, and Grok that signs in without a terminal: name a further
account, then follow the sign-in with its status, Grok's confirmation code, a
field for Claude's pasted code, and a link to reopen the page. A dropdown at
the bottom sets the default agent without launching it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Draw the Fable tick in its meter's own color
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Revert "Let omarchy-default-agent set the default without launching it"
This reverts commit da96261f7d26bb76774cbdc1cb9e0abde92bb841.
* Make the first agent signed in the default when none is picked
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep the agents panel in the bar, with setup, starters, and adding in it
A machine with no agent opens the panel on setting one up. Once set up, the
list ends with starter prompts for a new theme, plugin, or app, and a quiet
Add a subscription. The panel no longer sets the default agent, and the hero
drops its add button.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Offer the starter prompts as tiles, and adding as a row beneath them
Theme, Plugin, and App each get a tile with its glyph, and Add a
subscription a matching row with the + in a tinted square. The panel is
allowed to grow tall enough to show it all without scrolling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Put the tinted add button in the agents panel's hero corner
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Show when a limit resets in small type under its meter
The percentage keeps the right edge to itself, so the meter runs longer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Revert "Show when a limit resets in small type under its meter"
This reverts commit 108d9de59da1af12cddbf232b0b5333ad8994c64.
* Put each account's plan beside its name, and its email in a tooltip
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Stack each limit's percentage over its time left, so the meter runs longer
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Show only each limit's time left, with the exact percentage on hover
The meter already says how full a window is. Dropping the percentage beside
it leaves one calm figure per row, and the row's tooltip carries the number.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sign an existing account in again with omarchy-agent-account-add --reauth
It signs in where the account already lives: the CLI's own home (:primary)
through the normal browser, an added one through a private window. The usage
records now say which account is the primary.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Offer Sign-in required in the agents panel instead of how old the numbers are
A lapsed sign-in shows as a link that signs that account in again right in
the panel. The "as of" and "last known" notes are gone; trouble that isn't
about signing in still shows as text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep the separator dot out of the Sign-in required link
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drop the Setup > Agent Accounts menu and the stale plan
The agents panel now lists, switches, and adds accounts, so the menu's
duplicate of it goes, and the plan written before the design settled no
longer describes it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Install the compiler and Qt pieces for building Omarchy-style apps
base-devel plus qt6-base, qt6-declarative, qt6-multimedia, and qt6-wayland,
which is what Hype, Monologue, and Omacut build with through qmake6 and make.
Qt was only there as a dependency of those apps, and the compiler not at all.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add an omarchy-app agent skill for building apps the Omarchy way
It teaches how Hype, Monologue, and Omacut are built: C++ and Qt Quick in
one flat project, qmake6 and make into a single binary, a theme that follows
Omarchy's accent live, portal dialogs, keyboard-first conventions, Qt Test
offscreen, and a PKGBUILD that puts the app in the launcher, with starter
files that build and pass their tests as written. The agents panel's App
starter uses it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Link the omarchy-app skill on existing installs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Let Cancel stop a waiting login, and list a lone account's limits
Bash holds a trap until the foreground command finishes, so a login waiting
on the browser outlived Cancel. It now runs behind an interruptible wait.
With one account the usage record keeps its limits at the top level, which
omarchy agent account list now reads for the primary.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Re-read identity even when signed out, and count Grok as set up
A home whose login is gone no longer keeps the identity the registry saved,
so it reads as signed out and can be added again. The agents panel leaves its
setup screen once any agent is signed in, not only once one has usage to
show, since Grok has no usage collector.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Save each account's current identity before collecting usage
A home signed in to someone else since it was added kept the old email in
the registry, which the usage records name accounts by. The usage update now
refreshes the registry from each home first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Offer Claude's paste field from the start, and keep providers with accounts
Claude's login prompts for a pasted code without a newline, so the panel's
line reader never saw it; the field is simply there for Claude sign-ins. A
provider with accounts stays listed even when the active one's limits are
unavailable, so its other accounts can still be switched to.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Never autoswitch to a signed-out account, and report Codex's missing sign-in
An account nobody is signed in to is left out of switching, however much
room its cached numbers show. Codex answers a home without a login with an
error, which now reads as Waiting for auth, so the panel offers Sign-in
required for Codex accounts too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Link shared files before they exist, and only call every account over when it is
A shared file the primary home didn't have yet was left unlinked, so an
added account made its own copy and the two diverged for good; it's linked
up front now, and written in the primary home by whichever account writes it
first. The all-accounts-over notice waits until every account's limits are
actually known.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Make the agent starter tiles compact and call the section Make something cool
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Hold the agents hero line until its next fade
Every usage record that landed rebuilt the summary phrases, and the hero
indexed that live list, so opening the panel could swap the line several
times between fades. It now keeps what it shows until the timed swap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Start the default agent from the agents panel hero
A console button beside the add button runs the same launcher as the
right click: the default agent, or the picker when none is set. It hides
while adding a subscription, where the add button becomes the way back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Put the add button before the agent launcher in the hero corner
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Bring keyboard navigation back to the agents panel
The one-page redesign dropped left and right with the per-agent pages,
leaving the arrows only to scroll. They now walk everything that does
something, row by row: the hero's add and launch buttons, each
switchable account, and the starter tiles. Enter acts on the cursor,
the cursor scrolls into view, and hovering moves the same cursor so only
one thing is lit. Number keys still jump to an account.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Let adding an account take over the agents hero
While adding, the hero's line reads "Add an account" in place of the
rotating summary, and the X in its corner is the only way back, so the
add view drops its own title and Back link. Each agent to add is just its
mark and name; one that can't be added is dimmed and says why on hover.
The arrows walk that list too, and Enter picks one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Show the agents to add as large marks, three across
Each agent is a large mark over its name with no box around it, in one
row the arrows move along. The chosen one turns accent and grows a touch.
The reason an agent can't be added is shortened to fit inside the panel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Move between Autoswitch and Use on an account with the arrows
An account that isn't active is now two stops, Autoswitch then Use, so
left and right move between them and Enter acts on the one that's lit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Land on Use when moving up or down onto an account
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Hide Use on an autoswitching account until you're on its line
With Autoswitch on, the line shows only Autoswitch. Use appears when the
line is hovered anywhere or the keyboard is on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Start adding an account with the first agent focused
Opening the add screen puts the keyboard cursor on the first agent, so
Enter picks it straight away. A focused agent lights up even before the
check says whether it can be added.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drop the Sign in link under the account name field
Enter in the field already starts the sign-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Key the primary Claude account's limits cache by its subscription
The primary home always used claude-limits.json, so signing it in to
another subscription could carry the old one's numbers over when the
first probe failed, and autoswitch would act on them. Once the home says
who it's signed in as, its cache is keyed by that like every other
account's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Leave Qt Multimedia out of the base packages again
Quattro dropped it once the shell no longer needed it, and the app skill
already has an app that plays audio or video add it and list it in its
own depends. The compiler and the rest of Qt stay.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Render the agents panel's dynamic text as plain text
Sign-in status, help text, and provider names come from outside the
shell, so none of them should be read as markup.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep an account's plan and trouble clear of its Use and Autoswitch links
The details beside an account's name grew to their full width, so a long
plan or warning could run under the links on the right. They now shorten
with an ellipsis instead, and Sign-in required keeps its whole width.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Let primary name the first account, as the manual says
`omarchy agent account rename primary Hey` was documented but failed,
since the primary account's id is main and lookups took exact ids only.
primary now reaches the account marked primary, whatever it's been
renamed to, and no new account can take primary as its id.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Install everything an Omarchy app needs by default
The app skill builds with Qt Multimedia, SVG icons, and ffmpeg as well as
the compiler and the rest of Qt, so qt6-multimedia, qt6-svg, and ffmpeg
join the base packages and the migration. The shell still plays no video
through Qt Multimedia, which is what its test now checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Dim agent limits kept from an earlier check, with their age on hover
When a Claude probe fails, the last numbers carry on and looked just like
fresh ones. Those meters now dim, and their tooltip says how old they
are. The record carries limitsStale and limitsFetchedAt for this.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Give the app skill templates for every file its build needs
The templates named src/backend.{h,cpp} and a test file without showing
them, so each app had to invent its own. They're now templated, with a
starter icon, a note that LICENSE and the icon must exist for package(),
and qt6-wayland in the PKGBUILD's depends. Scaffolded from the templates
alone, the app builds and its tests pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Start the agents panel cursor over when the agent list comes or goes
Right after the shell starts, the panel can briefly look like a first
setup and focus the first agent to add. When the records land the rows
change under the cursor, which then lit an account nobody had picked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drop dead hover and limit checks from the agents panel
The tiles and hero buttons fell back on their own hover when there were
no keyboard rows, but the hero always has one, so hover only ever moves
the cursor. Stale meters only exist when there are limits, so neither
the panel nor the record needs to check for some.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drop the a hotkey for adding an agent subscription
The + in the hero is the way in, by mouse or by arrowing to it. The
plugin README also catches up with the panel: the launcher, dimmed stale
limits, the new add screen, and the arrows walking a cursor rather than
scrolling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* List a command family that sits under a flags-only command
`omarchy agent account` reached omarchy-agent, which takes only flags,
and failed on the word instead of listing the account commands. When the
command matched so far takes only flags and the next word names visible
commands, the router now lists them. `omarchy update aur` likewise lists
the update aur commands rather than running a full update.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Refuse to remove an agent account a running session uses
Removing an account deleted its home at once, pulling the login out from
under any session started in it, though running sessions are never meant
to be touched. It now says to quit that session first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Prefer an account checked just now when switching near a limit
Autoswitch weighed numbers kept from an earlier check like fresh ones.
A parked account's sign-in lapses within hours, so a stale one stays a
candidate, but an account checked just now wins over it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Leave a user's own omarchy-app skill in place when linking ours
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Reach Sign-in required with the keyboard in the agents panel
Both the link on a lapsed account and the one on a single-account agent
are now stops in the cursor's walk, and Enter signs in again. A picked
link scrolls into view.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Bring the manual's agent accounts paragraph up to date
Near-limit checks run every three minutes, not every minute, and
Autoswitch now appears on hovering an account's line and takes Use's
place while it's on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep the agents panel cursor on things you can act on
The active account was a stop with nothing to do, so the cursor seemed to
vanish there. It now only stops on it to sign in again. The hero's
buttons also show the cursor plainly, with an accent border and a deeper
tint instead of a shade's difference.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Never leave an added agent account's login outside the registry
Registering moved the login into its home before carrying settings over
and saving the registry, so a failure in either stranded a home holding
a sign-in that the account commands couldn't see. Settings are now
carried while the login is still pending, and a failed save moves the
home back there for the add command to clean up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Put room before freshness when picking an account to switch to
Preferring accounts checked just now outright could pick a fresh one at
94% over a stale one at 10%. An account within 15 points of the
threshold now counts as near its limit however fresh, so accounts with
room come first and freshness only decides among them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Check agent limits more often relative to the switch threshold
Faster checks started at a fixed 80%, so a threshold set lower could be
crossed and wait out the 15-minute interval. They now start 15 points
below the threshold, which is still 80% at the default 95%.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With both at 0 the monitor is disabled and handleIdleChanged returns early, so a cycle already running when shell.json changed never cancels: omarchy-system-wake never runs, and if the screensaver never opened a window nothing else ends it. Cancel it the way turning on stay-awake does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex Medium <noreply@openai.com>
With 0 meaning off, setting the screensaver to 0 while the screensaver is up moves the first idle deadline to the lock's, which turns the pending lock's bound interval to 0 and locks at once, minutes early. Set each timer's interval when the cycle starts, so a change to shell.json takes effect from the next cycle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex Medium <noreply@openai.com>
The timers' intervals are bound to the configured delays, so setting the lock or screensaver to 0 while the screensaver is up turns a pending timer's interval to 0 and it fires at once. Check the action is still enabled when its timer fires.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No suite failed with the strip restored, so nothing kept the 4.0.3 regression from coming back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bar widget instances are not handed sourceDir (ModuleSlot.injectProps sets only bar, moduleName and settings), and a plugin could reach its directory through Qt.resolvedUrl, so the comment's rationale was wrong on both counts. Two lines carry what the code cannot say.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex 0.158's app-server can leave account/read unanswered, and asking it
first lost the limits whenever it did, leaving the agents panel showing
"Codex limits unavailable". The limits name the plan themselves, so they're
asked first and account/read is only a short fallback when they don't.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Under the Lua config system, 'hyprctl keyword monitor ...' is rejected
("keyword can't work with non-legacy parsers") yet still exits 0, so the
panel's enable/disable row silently did nothing. Use the hl.monitor eval
API instead, as omarchy-hyprland-monitor-scaling already does.
The re-enable must pass disabled = false explicitly: hl.monitor rules
merge, so a disabled = true already in config (e.g. the
internal-monitor-disable toggle file) otherwise survives and the
re-enable no-ops.
fzf's completion.bash rewraps the completions of ~76 commands on every
interactive shell start, costing ~25ms once bash-completion is loaded. Keep
only the key bindings (Ctrl-R, Ctrl-T, Alt-C); **<Tab> fuzzy completion goes.
`starship init bash` only prints a line that runs starship again with
--print-full-init, so call that directly and start the binary once.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The fake nodes carried only id and name, so a projection that handed back the node passed every assertion. Giving one an object-valued audio property, as a live PwNode has, makes the deep comparison catch it.
Co-Authored-By: Codex Medium <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A machine without a GPU, like most VMs, renders through llvmpipe on the
CPU, where every animated frame and every translucent window costs. In a
VM, opening and closing a terminal took ~5s of CPU; a panel ~3.4s.
omarchy toggle animations (also under Toggle > Animations) places a Hyprland
flag that turns off animations, blur and shadows and makes windows opaque.
The shell follows Hyprland's animations:enabled, rereading it on every
config reload: its one-shot animations run for Style.duration(ms), which
is then 0, and its spinners, pulses and title marquee hold still. A new
install in a VM starts with the flag in place.
Measured in the ISO test VM (llvmpipe), CPU per interaction:
terminal open+close 5000ms -> 481ms, workspace switch 1670ms -> 409ms,
audio panel 3446ms -> 796ms, volume OSD 2324ms -> 584ms, menu 2028ms ->
1241ms.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Hand browser and web app launches to the running browser directly
With a Chromium-based browser already running, each launch started a second
browser process only to pass its command line over the profile's singleton
socket and exit, ~165ms before the running browser even heard of it.
omarchy-cmd-browser-handoff sends that message itself with socat and waits
for the ACK, so a web app opens in ~210ms instead of ~430ms.
It hands off only what it can do exactly as the browser would: the packaged
browser binary rather than a wrapper, a flags file holding nothing beyond the
process flags Omarchy ships, no other data directory, and a socket we own.
Anything else, or no answer, launches the browser as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Hand Brave Origin launches to the running browser too
/usr/bin/brave-origin is the package's own launcher, reading
brave-origin-flags.conf, with the profile in BraveSoftware/Brave-Origin.
A web app now opens in ~158ms instead of ~345ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Collapse identical notifications from the same sender into one toast
A web app open in several tabs fires one notification per tab for a single
reminder, stacking identical toasts. A new notification matching an on-screen
toast's app, summary, and body now replaces it like a replaces_id update.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Only collapse live duplicates that share image and click target
Screen recording toasts share text but preview and open different files, and
replayed history rows share images with entries still in history.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Compare a notification's current content when collapsing duplicates
A replaces_id update can land while the insert is deferred, so the initial
snapshot may no longer describe what the notification says.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
To open a new terminal in the focused terminal's directory, the launcher
asked Hyprland for the active window with hyprctl, ~10ms on every
SUPER+RETURN. The binding now reads the pid from hl.get_active_window() and
passes it as --pid. Without one, from the menu or a shell, the launcher asks
Hyprland as before.
The bind is now a Lua function, which Hyprland reports only as __lua, so
o.bind_commands records the command it stands for and the keybindings menu
still launches a terminal when the entry is picked.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Read the default browser from mimeapps.list when launching
omarchy-launch-browser and omarchy-launch-webapp asked xdg-settings for the
default browser on every launch, ~72ms through several POSIX shell scripts.
The new omarchy-cmd-default-browser reads the http handler from the
config-dir mimeapps.list files the way xdg-mime does, desktop-specific list
first, taking the first listed entry whose command is installed, in ~10ms.
With nothing set there it falls back to xdg-settings as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Let the first copy of a browser entry decide whether it is installed
A user-local entry whose browser is gone could be rescued by a system copy
of the same ID, which the launchers never read. xdg-mime judges the first
copy found, as the launchers use it, so do the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Add SSH Agent as an optional service install
Enables gcr-ssh-agent (already shipped via the gnome-keyring dependency)
so passphrase-protected SSH keys work from any context: the agent prompts
graphically on first use, with opt-in passphrase storage in the keyring.
Adds install/remove commands and Install > Service / Remove > Service
menu entries.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BReZWvvLsDRUaqgjiRzvZ5
* Clear the agent's SSH_AUTH_SOCK when removing the SSH agent service
The socket's ExecStartPost exports SSH_AUTH_SOCK into the user manager with set-environment, which disabling the socket does not undo, so apps launched after removal still pointed at a stopped agent. The environment.d file also has to be deleted before disable reloads the manager, or the generator re-exports it. Only an SSH_AUTH_SOCK pointing at gcr is cleared, so another agent's stays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Move SSH Agent setup to Setup > Security
Turning on an SSH agent is security configuration of the machine rather than installing software, so it sits with SSHD and the other Setup > Security entries. It hides once enabled, as Sudoless Docker does, and Remove > Service > SSH Agent turns it back off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
The wizard writes its config while prompts remain, so the watcher starts probing a gateway that is not up yet; each probe hangs until its 10 second timeout, and OpenClaw's CLI, killed by it, takes the terminal it inherited on stdin out of raw mode on the way out. The wizard does not set raw mode again, so from then on its prompts echo keys instead of reading them: the first letter typed at the channel search landed and the next one was printed below the prompt. The probes now read from /dev/null. This is the change reverted earlier on a test that only ever saw probes that exited on their own, never one the timeout killed.
Installing a service enables and starts it, so moving an old-package gateway or node host the user had stopped or disabled turned it back on. What was running and enabled is read before the move and put back after it.
Co-Authored-By: Codex XHigh <noreply@openai.com>
A service counted as the runtime's, or as a half-moved old one on /usr/bin/node, whenever it ran anything under ~/.openclaw, so a script of the user's in that directory could be stopped and rewritten, and seeding could go ahead beside it. The runtime is now OpenClaw's own script as upstream's installer lays it out, under ~/.openclaw/tools/*/lib/node_modules/openclaw, and both checks use that one definition.
Co-Authored-By: Codex XHigh <noreply@openai.com>
Judging a moved service by whether its program sat under ~/.openclaw missed a retry after a half-finished move, whose unit already ran the runtime's script on /usr/bin/node and so was never selected again; misread a symlinked home against the resolved Node it pins; and would have moved a gateway the user runs on Bun. A service now needs moving while it runs the old package's script, or the runtime's script on /usr/bin/node, the Node the old package pulled in; any other runtime is left alone.
Co-Authored-By: Codex XHigh <noreply@openai.com>
Since 2026.9.6 upstream's installer keeps the Node a gateway service already ran when it rewrites the unit, so moving a gateway the old package installed left it running the runtime's code on /usr/bin/node, from the nodejs package the old openclaw package pulled in and the seed no longer depends on. A moved service now counts as moved only once its program is under ~/.openclaw, and the reinstall pins the runtime's Node with --runtime-path, which gives the same unit a fresh install gets.
Hype joins Omacut, Monologue, Omacalc and Omawrite as an Omacom app in
the base install and the preinstall set, so Remove and Install
Preinstalls cover it too. A migration installs it on existing machines,
unless their owner has removed the preinstalls, and the GUIs chapter of
the manual introduces it.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Flip Elsewhen between Fahrenheit and Celsius with Shift+T
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Flip Elsewhen's temperature units with Alt+T instead of Shift+T
PanelKeyCatcher's textKey now carries the held modifiers, which is how the
panel tells Alt+T from T. Existing handlers take only the text and are
unaffected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A row lit itself by lifting its own time-of-day fill, so a picked night row
could read darker than the daytime rows around it. The picked city now takes
midday's lifted fill, the brightest, whatever its time of day; hover still
lifts each row from its own.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Monologue is Omarchy's own webcam recorder: pick a camera and microphone
once, press Space to record, and stop straight into a built-in editor
to split, trim and remove clips before saving an MP4.
Ship it in the base packages alongside Omacut, include it in the
preinstall restore/remove lists, and describe it in the manual. A
migration installs it on existing machines unless the user removed the
preinstalls.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Flip Elsewhen between 24-hour and AM/PM time with t
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Line up Elsewhen's weather in one column
The temperature and weather icon sat against each row's time, which is
narrower for "4:06" than "10:06", and shared the name's slack. The time
column now takes the widest time's width in every row, and a spacer holds
the weather against it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Navigate Elsewhen from the keyboard and summon it with Super+Ctrl+Alt+E
Up and down walk home and the cities, lighting the picked row and turning
the globe to it. Left and right move the clocks an hour, held until Escape
or close. Escape now clears a shifted time before leaving the globe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Reach Add a city from the keyboard and shift time past a day
Down past the last city selects Add a city, where Return or Space opens the
search. The arrow keys no longer stop at twelve hours, and shifts past a day
read in days.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Delete the picked Elsewhen city from the keyboard
Delete (or x) removes the picked city and moves the cursor to the one that
takes its place. The picked row shows its × so the target is plain. Delete
now reaches every panel's deleteRequested, as it already does in the
clipboard and the menu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Center bar glyphs on the open-panel underline
The 16px icon canvas in the 27px slot was snapped from 5.5 to 6, and the
glyph's fractional centering correction was snapped too, so glyphs sat up to
0.9px right of the underline, which centers on the slot. Both are now placed
exactly, and the geometry test measures against the slot to within 0.05px.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Answer the Elsewhen panel over the shell's IPC socket
The Elsewhen panel arrived with a plain IpcHandler, so omarchy-shell
reached it only through the slower qs ipc fallback, and the test that
every first-party handler registers as ShellIpc failed on quattro.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Remove the IPC socket of each shell a test starts
The runtime smoke and screenshot sanity tests start the real shell from a
temporary copy and kill it, which leaves its IPC socket behind in
XDG_RUNTIME_DIR. A session that runs the suite a few times collected
dozens of them.
base-test.sh gains shell_ipc_socket, which derives a shell's socket as
omarchy-shell does, and both tests remove theirs in cleanup. The socket
test derives its fixture's socket through it too, so the helper cannot
drift from omarchy-shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Every plugin change handed the panel Instantiator a fresh array, which
rebuilds every panel. The panels were rebuilt four times in the first
700ms of startup, each rebuild starting a new asynchronous load of every
keepLoaded panel while the last was still in flight. Under load two OSDs
could run at once, each registering the osd IPC handler: the runtime
smoke test's intermittent "another handler is registered for target
osd".
The entries now live in a ListModel synced in place. A panel whose
plugin still loads the same way keeps its loader, so startup adds each
panel once, and a full plugin reload still clears the model to rebuild
from fresh code.
Under a six-core busy load the smoke test hit the duplicate in 1 of 5
runs on quattro and in none of 10 with this change.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A city on this machine's zone becomes homeCity, the zone's own city clears
it, and a city on another zone leaves it alone. Ported from omacom/elsewhen#5.
Co-authored-by: Shawn Yeager <shawn@shawnyeager.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Move Elsewhen into Omarchy as omarchy.elsewhen
The world clock ships in the shell tree instead of its own package. A
migration renames existing bar entries with their settings and removes
the retired package.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Remove Elsewhen's shelved Earth row, overlap band and sky tint
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tidy Elsewhen's models, data helper and docs, and drop currency
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Restructure Elsewhen's panel and globe on the shared shell components
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Show Elsewhen's bar globe upright
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Use the shared city search for Elsewhen's globe jump bar
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep legacy Elsewhen entries single and retry offline geocodes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fetch Elsewhen's weather with curl instead of a Python helper
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The OSD, the reminder flow and the Wi-Fi QR code now keep a surface that
parks as a 1x1 in the corner and grows when shown. They were missing from
the layer rule that turns Hyprland's layer animation off for the shell's
overlays, so Hyprland animated that growth: the OSD flew in from the left.
Add them to the rule, and test that every OverlayWindow's namespace is in
it.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Answer omarchy-shell calls over the shell's own socket
Every omarchy-shell call started a qs ipc client, ~45ms of startup for one
IPC call: a theme switch makes two, and every script-driven OSD, toggle
refresh and lock query paid it too.
The shell now serves a socket in XDG_RUNTIME_DIR, named from its config
path and Wayland display as qs ipc selects its instance, and omarchy-shell
tries it first through socat, which starts in ~5ms. First-party handlers
register as ShellIpc, an IpcHandler that qs ipc still reaches, and the
socket calls only the functions a handler declares with their exact
argument count, allowed by name so QObject methods such as destroy() stay
out of reach.
When the shell ran nothing it answers SKIP, and omarchy-shell asks qs ipc
for its exact answer, so errors, third-party plugins and an unreachable
socket behave as before. A call that may have run is never retried: a
timeout or a connection closed without an answer reports the shell as not
responding.
omarchy-shell shell ping takes ~13-18ms instead of ~61ms, and omarchy-osd
reaches the screen in ~36ms instead of ~77ms. Output and exit status match
the qs ipc path across 26 calls, errors and quiet mode included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Only accept whole socket replies and retry only unmade connections
A reply cut off after its OK prefix passed for the whole answer, and an
empty reply with socat failing was retried through qs ipc although the
request might already have been delivered.
An answer now counts only once its record separator arrived. socat's own
errors join the reply, so only its connect error, a socket nothing
listens on, falls back to qs ipc beside an explicit SKIP; anything else
is reported as not responding rather than retried.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Add a theme-set sample hook that mirrors themes to herdr machines
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Mirror theme changes to herdr machines by default
Replaces the sample hook with omarchy-theme-sync, which omarchy-theme-set
starts detached after every theme change, and a theme-sync-off toggle that
stops a machine from both sending and receiving.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Rename omarchy-theme-sync to omarchy-theme-set-herdr-machines
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Label the toggle Herdr Theme Sync and extract its menu guard
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Answer the Herdr Theme Sync menu guard from omarchy-theme-set-herdr-machines
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sync herdr machines one at a time and drop the dry run
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Check the theme sync toggle under the lock and stop special-casing this machine
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Make herdr theme sync opt-in
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The lock started decoding its wallpaper only once locked, with the cache
off, and first at the view's unsized native resolution. A machine
suspending right after locking froze that decode partway, so waking
showed the password field on a bare background and the wallpaper
popped in after it. On this machine the wallpaper took ~208ms to become
ready, and the suspend followed the lock by 66ms.
The lock service now keeps each screen's lock wallpaper decoded in the
image cache, as the lock view requests it: same URL, the screen's
logical size, PreserveAspectCrop. The view waits for its size and reads
from the cache, so the wallpaper is ready within ~3ms of the lock
starting. The version in the cached URL follows the file's mtime and
size, so a wallpaper overwritten in place still reloads.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Each brightness key ran omarchy-brightness-display: resolving the focused
monitor and backlight device, checking for an Apple display, reading,
writing and reading back through brightnessctl, then omarchy-osd over a
qs IPC client. That is 64-133ms from keypress to OSD depending on load,
on keys that repeat while held.
The brightness up and down keys now dispatch global shortcuts. On an
internal panel the shell reads the level from sysfs, steps it by the
script's rules, writes it with one brightnessctl, and shows the OSD from
the level read back. A press overlapping one still being applied is
dropped, as the script's flock drops it. External and Apple displays
fall back to the script, which drives them over DDC or their own helper.
The absolute and precise brightness keys still run the script.
Across 24 steps from eight starting levels the shell and the script land
on identical levels, and keypress to OSD drops to ~31ms.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
omarchy-launch-webapp asked xdg-settings for the default browser with
BROWSER still set to omarchy-launch-browser, which sends it down a
slower path to the same answer: ~105ms instead of ~30ms on every web app
launch. Unset it, as omarchy-launch-browser already does.
omarchy-cmd-terminal-cwd found the focused terminal's shell with pgrep,
which scans all of /proc on every new terminal. Read the terminal's
child lists from /proc instead, keeping the newest child as before.
Web app wrapper overhead drops from ~111ms to ~34ms, and a new terminal
appears in ~77ms instead of ~90ms.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Step the volume in the shell instead of a script per keypress
Each volume key ran omarchy-audio-output-volume: several pactl calls in
bash, then omarchy-osd delivering the OSD over a qs IPC client, ~165ms
from keypress to OSD, on keys that repeat while held.
The volume up, down and mute keys now dispatch global shortcuts that the
media service handles over PipeWire, stepping, clamping, unmuting and
debouncing by the script's rules and showing the same OSD. It acts only
when the default sink is an ALSA sink, which is its own physical sink.
Any other default, a DSP chain or EasyEffects above all, falls back to
the script, which resolves the physical sink from the live routing on
every press. The precise +1/-1 keys still run the script.
Keypress to OSD drops from ~168ms to ~20ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Run media and notification keys in the shell without an IPC client
The media keys and the notification dismiss, invoke and history keys ran
omarchy-shell, starting a qs client for one argument-free IPC call.
A new ipc shortcut kind names such a call as target.method. The shell
hands it to the service that owns the target, which runs its own
IpcHandler function, so the key behaves exactly as the omarchy-shell
call did. A call missing from the list binds through omarchy-shell.
Dismissing a notification with SUPER+comma clears the popup in ~11ms
instead of ~40ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(screensaver): open in own special workspace instead of active
- The screensaver window rule sets fullscreen, and a workspace holds
only one fullscreen window
- Launching it on the active workspace drops the fullscreen window
already there, and leaves it windowed after the screensaver quits
* Keep a showing special workspace open through the screensaver
A monitor shows one special workspace at a time, so opening the screensaver on its own replaced a visible scratchpad, and it stayed hidden after the screensaver closed. Open it on the special workspace that is already showing instead; it covers that just as well, and the workspace is still there when it goes.
The test's stubbed event stream follows the one in #10870.
Co-Authored-By: Willem van Ede <37050539+WillemCR@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Hand focus back after the screensaver closes
Hyprland focuses a monitor when its special workspace empties, so on more than one monitor keyboard focus ended up wherever the last screensaver happened to close, rather than where it was. A detached reader on the launcher's event stream waits for the last one to go and focuses the original monitor again; the launcher itself still exits straight away, as the idle service expects.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep a remapped screensaver off the regular workspaces
The terminal occasionally maps its window again as it closes, after its special workspace is gone. The launcher's workspace only applies to the first map, so the window landed on the focused monitor's regular workspace, where its fullscreen rule took fullscreen from the window there. On Hyprland 0.56.2 with two monitors this happened in 4 of 45 cycles, each time losing the user's fullscreen. A class rule now sends any later map to a hidden special workspace; the launcher's exec rule still wins on the first map.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Check for screensavers before waiting on their close
The focus watcher only looked for remaining screensavers after reading a closewindow, so one that never mapped, or closed while the launcher was still waiting on another monitor, left it blocked until some unrelated window closed, and then it pulled focus back to the old monitor. It now checks before each wait, and matches the exact class, passed through the environment so the screensaver's pgrep and pkill never see it in argv.
Co-Authored-By: Codex Medium <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Willem van Ede <37050539+WillemCR@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Codex Medium <noreply@openai.com>
The menu, emoji picker, clipboard, OSD, reminder flow and Wi-Fi QR code
mapped a fresh surface on every open. Qt drew its first frames before
Hyprland sent the surface's fractional scale: the pixel ratio stepped
2, 1, then 1.6, so on a 1.6 display each overlay showed blurry for
~350ms before going sharp.
A new OverlayWindow keeps the surface. Hidden, it parks as a 1x1,
input-less layer below windows, off the overlay layer so it never
blocks direct scanout. Showing only resizes and raises it, so the scale
is already settled. Content stays hidden until the surface has grown,
so no 1x1 frame is stretched across the screen, and the window follows
the focused monitor each time it is shown. The image picker's own
fullscreen parking moves onto it too.
Each parked overlay keeps a Qt window alive: at rest the shell holds
~40 MiB more RSS and ~20 MiB more GPU memory.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Every binding that opened a menu or panel ran omarchy-menu or
omarchy-shell, which starts a qs client just to deliver one IPC call:
~60ms before the shell heard about the keypress.
The shell now registers a Hyprland global shortcut for each menu route
and panel in default/omarchy/shortcuts, and o.bind turns { menu = ... }
and { panel = ... } into hl.dsp.global for those, so a keypress spawns
nothing. A route or panel missing from the list binds through the
command as before. The default menu and panel bindings use the new form.
SUPER+SPACE opens the menu in ~31ms instead of ~94ms, measured from a
simulated keypress until the menu layer maps.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Speed up theme switch by backgrounding preload and parallelizing browser refresh
`omarchy theme set` was blocking on `omarchy-theme-switcher --preload` (~1.1s)
and serially refreshing every Chromium-family browser (~1.0-1.5s), making a
picked theme feel slow even though the shell recolored instantly.
- Background `omarchy-theme-switcher --preload` like the background cache
- In `omarchy-theme-set-browser`, skip the privileged policy write and browser
refreshes when every existing `color.json` already has the requested color
- Refresh only running browsers, and run those refreshes in parallel
Fixes#12627.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* Harden the unchanged-color shortcut in omarchy-theme-set-browser
Review feedback identified that the original shortcut could vacuously skip
when no managed policy directories existed, and that it accepted a color.json
whose ownership or contents the privileged writer would have rewritten.
- Only skip when at least one managed directory exists and every existing
color.json is a regular, root-owned 0644 file containing the canonical JSON
- Add OMARCHY_BROWSER_POLICY_DIRS as a test-only override so the setter's
unchanged-color check does not depend on the host's real /etc policy files
- Update browser-policy-sudoers-test.sh to use that override, and make
theme-set-browser-test.sh behavioral rather than grep-only
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* Find running browsers with one process scan during the policy write
Each browser check ran its own pgrep, and every pgrep rescans /proc,
~40ms apiece. Read the process table once with ps while the policy
writer runs, keeping its stdin so a terminal sudo prompt stays in the
terminal, then refresh the running browsers in parallel.
Chrome's fallback to plain google-chrome never ran: the refresh helper
returned success for a missing browser, so the || branch was dead. Pick
whichever Chrome binary exists up front instead.
The unchanged-color shortcut keeps its root:root 0644 check without the
fallback for hosts lacking stat -c, which Omarchy never runs on. Its
tests stub stat, ps and command discovery, so they neither depend on
nor touch the host's browsers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Decode the next theme background while the theme stages
The wipe waited 125-290ms after the transition arrived, decoding the new
wallpaper. Most stock wallpapers are WebP, which Qt decodes at full size
and scales afterwards, so a screen-sized sourceSize does not shorten it.
Start the decode earlier instead. omarchy-theme-set chooses the next
background and snapshots it before rendering templates, then sends a new
background prepare call in the background. The shell loads it into the
hidden incoming frame, so the transition finds it decoded. A prepare that
arrives after its transition is ignored, and one no transition claims is
dropped after five seconds.
The wipe now starts ~255ms after omarchy-theme-set begins instead of
~345-490ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Decode the wallpaper at screen size instead of shipped size
Wallpapers decoded at the resolution they were shipped at: a 5120x2880
stock wallpaper took 5120x2880 of RGBA on a 1920x1200 panel, and a
transition held up to three such frames. Bind sourceSize on the
displayed wallpaper and both transition frames to the screen's physical
size. PreserveAspectCrop treats it as the area to cover, so the image
still fills the screen.
Qt scales a decode up as well as down to cover sourceSize, so the native
size is read from the file header first with magick identify, and a
wallpaper smaller than the screen decodes at its own size. The images
wait for both sizes, so nothing decodes at native size first.
Ported from #8324 onto BackgroundMedia and the prepared incoming frame.
Measured with a 5120x2880 wallpaper, the shell's GPU memory at rest
drops from 264 MiB to ~148 MiB. The size probe delays the reveal by
~30ms, which the earlier prepare still more than covers.
Co-authored-by: Ryan Yogan <ryanyogan@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Ryan Yogan <ryanyogan@gmail.com>
The probes do not take the wizard's terminal out of raw mode: a raw pty stays raw across `openclaw dashboard --json`, and the channel search takes keys normally in foot and Ghostty with the probes running, with or without the change. The break seen at the channel prompt has another cause.
OpenClaw's CLI takes a terminal on stdin out of raw mode as it exits, and the onboarding watcher's `openclaw dashboard --json` probes inherited the wizard's terminal. Current releases write the config partway through the wizard, so the probes started while prompts remained, and every two seconds the terminal fell back to line mode: typing at the channel prompt echoed instead of selecting. The probes now read from /dev/null.
A theme switch waits on omarchy-theme-set-templates before the shell
transition starts, and it spent ~670ms spawning processes: a subshell
per color for its rgb form, a grep per template per function family, an
awk per mix, and a sed per template matching every line against
hundreds of patterns.
Helpers now return through REPLY, one grep finds the function tokens
across all templates, and one awk computes the mixes with the same
floating point rounding and renders every template. Output is byte for
byte identical across all shipped themes. Generation takes ~65ms, and
the transition starts ~130ms after omarchy-theme-set begins instead of
~740ms.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Run menu summon actions in-process
A menu action that only summons another shell plugin spawned bash and a
qs ipc client to ask this same shell to do it, about 60ms of the path.
Call shell.summon directly instead, and fall back to bash when the call
is refused or the action is anything more than a bare summon.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Open the theme picker from rows held in the shell
Opening the theme picker ran omarchy-theme-switcher to rebuild its index
and then made a second IPC call, about 170ms before the picker mapped.
The picker now holds the theme rows itself, opens from them at once, and
refreshes them behind the open via omarchy-theme-switcher --print-rows.
It applies the chosen theme with omarchy-theme-set directly, so
omarchy-theme-set no longer preloads the picker.
From the keybinding to the overlay mapped drops from ~245ms to ~83ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep the image picker surface mapped between opens
Each open mapped a fresh surface, which rendered its first frames before
Hyprland sent its fractional scale: the pixel ratio stepped 2, 1, then
1.6, so the picker flashed blurry for ~130ms and re-uploaded every
thumbnail texture. Keep the surface and park it transparent and
input-less on the bottom layer while closed, since anything on the
overlay layer blocks direct scanout for fullscreen apps. It follows the
focused monitor on each open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
5db4a401 fixed the installer, but machines already installed offline
still pin Node to the ISO's exact version, so mise up never updates it.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
omarchy-audio-output-sink follows a DSP sink to the physical sink through the
DSP's playback stream. EasyEffects 8 has no such stream: its output node links
its ports straight to the sink's playback ports, so the search found nothing
and the volume keys, the bar's audio panel and the output switcher all acted on
easyeffects_sink itself. That sink's volume is inert -- its monitor, which
EasyEffects reads, does not follow it -- so the OSD moved while the speakers
did not.
Follow the port links to the alsa_output sink instead, and when EasyEffects is
idle and has dropped them, use the output device its settings name, if that
sink exists. Filter-chain tunings keep resolving through their stream as before.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Stopping stay-awake sends TERM to the held process, which was an exec'd
sleep. systemd-inhibit reports a child killed by a signal as an error, so
every update ended with "'/usr/bin/setpriv' terminated by signal TERM."
The held process now stays a shell that traps TERM, kills its sleep, and
exits cleanly.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
By name, the sudo call resolved through secure_path, which puts /usr/local/bin ahead of /usr/bin and falls back to the caller's PATH where no secure_path is set, so an install could run a different helper than the pinned path did. $OMARCHY_PATH/bin is the package's symlink into /usr/bin on an install and the checkout under a dev link, so neither PATH nor secure_path takes part.
Co-Authored-By: Codex XHigh <noreply@openai.com>
Pinning /usr/bin ran the packaged helper even where the migration came from somewhere else. Under a dev link the migration is read from the checkout while /usr/bin still holds the last installed package, and a package predating __migrate prints its usage and fails the update. By name, the unprivileged call goes through PATH and the sudo call through secure_path, which is how other migrations reach their helpers and which lands on /usr/bin on an install.
Co-Authored-By: Codex XHigh <noreply@openai.com>
* Ask for the sudo password once per omarchy update
Every sudo call in omarchy update prompted, because the no-update wrapper
covered the whole run on top of per-phase revokes, and stay-awake revoked
the timestamp on its own entry and exit. A single update could ask four
times before the snapshot finished (#13319).
Authorize once, right after confirmation, starting from a revoked
timestamp so the prompt always belongs to this update. A background
keepalive refreshes it until the update is done. Prune, snapshot,
stay-awake, keyring, system packages, migrations, orphan removal, service
restarts, the post-update hook, and mise all share that authorization.
AUR builds run third-party PKGBUILD code, so they move to the end and run
cold: the keepalive stops, the timestamp is revoked, and yay and any bare
sudo use the no-update wrapper. The timestamp is revoked again after AUR
and on every exit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep the single authorization for passwordless sudo and ttyless inhibition
Authorize by running a command instead of sudo -v. Under the default
verifypw=all, -v prompts even when passwordless sudo is enabled, which
would have added a prompt those users never had.
Inside an update without a terminal, stay-awake now reuses the update's
authorization with a non-interactive sudo instead of asking again through
polkit. It falls back to polkit only if that authorization is gone.
The test sudo refuses a cold non-interactive call, as the real one does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A unit is Omarchy's or the old package's by the program its ExecStart runs or the script it hands that program, not by a path appearing anywhere in the line, so a gateway that merely names ~/.openclaw in an argument is not taken over. The state directory and config overrides are cleared with the profile selectors, since Omarchy's OpenClaw is the default one. Any failure after a running service was stopped for the move names that service, a partial stop included.
Co-Authored-By: Codex XHigh <noreply@openai.com>
Upstream's installer rewrites a loaded gateway onto the new copy but only warns when it will not start, which let the install finish with no gateway running. A service that was running before the move now has to be running after it, or the install fails and says the service is stopped; a failed seed says so too.
Co-Authored-By: Codex XHigh <noreply@openai.com>
Removing the runtime while keeping ~/.openclaw left state that its own updates had migrated past the packaged release, and reinstalling seeded a release that refused to open it; even `openclaw update` refused. Removal now takes openclaw off PATH and leaves the runtime inside ~/.openclaw with the state, where a reinstall picks it back up; deleting ~/.openclaw still takes both.
Upstream's installer runs `gateway install --force` on any gateway it finds loaded, so the old package's gateway has to be stopped before seeding rather than after, and a stop that fails ends the run. The profile selectors are cleared for the whole command, installer included, and every refusal now comes before the package is installed.
Co-Authored-By: Codex XHigh <noreply@openai.com>
The first E2E run of the migration found that the new runtime cannot install its gateway service while the old package's gateway, still running from deleted files, holds the state directory, so that service is stopped first. Removal also takes the unit backups `openclaw update` leaves behind.
Codex's review found the rest: upstream's installer takes over any loaded gateway service, so a gateway running another OpenClaw, a foreign command on PATH or an openclaw shadowing it are refused before anything is set up; root is refused before --check runs the user's wrapper; --check needs the package, since --now would otherwise run pacman where no terminal can ask for a password; and moving a service clears the selectors that would aim the install at another unit.
Co-Authored-By: Codex XHigh <noreply@openai.com>
With an empty package list the trailing && left the helper with exit
status 1, which stopped the Steam installer before Steam was installed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the venv active, a gateway started by hand is `python -m hermes_cli.main gateway run`: the interpreter is bare, the executable resolves outside the runtime, and no runtime path is on the command line, so the removal took it for a stranger and refused over the files it holds rather than closing it. Hermes's own package named with -m is the program.
Co-Authored-By: Codex XHigh <noreply@openai.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Remove > AI refused whenever anything had Hermes's files open and told the user to close it and try again, and the thing open was Hermes: the agent in the terminal that choosing it as the default agent leaves running, the desktop app, a gateway. Those are the removal's to close. It now stops the gateway unit that upstream's `hermes gateway install` wrote, since that unit starts the runtime about to be deleted and would start it again the moment it was killed, then ends every process whose program lives in that runtime or in the package, and only then refuses over whatever is left, which is somebody else's: an editor on a skill, a shell sitting in ~/.hermes, a writer on the state database. Both are judged by the program, never by a later argument or by the home served, so an editor opened on a runtime file is the user's and a unit running a Hermes kept elsewhere is left alone whatever home it serves; for an interpreter the program is the script it runs, so a gateway started by hand as `python .../hermes gateway run` is found too. The refusal comes before anything is touched, so a run that stops leaves Hermes running as it was. A unit that will not stop still aborts the removal, as dropping the package would strand a live gateway on deleted code.
Co-Authored-By: Codex XHigh <noreply@openai.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Use the installed executable for non-root self-elevation, including calls from a development checkout or symlink.
Reported-by: Aleksej <aleksejhairov@yandex.ru>
Resolve color references iteratively and use the caller's fallback when a reference repeats. Preserve normal palette roles, color parsing and gradient first stops.
Reported-by: Aleksej <aleksejhairov@yandex.ru>
The refusals that leave a self-installed Hermes alone came before the command was replaced only for a finished runtime. An unfinished one was still bootstrapped over a git status that could not be read, since a failed status read as a clean tree, and a half-built app beside it, or an edit the Linux runtime patch could not land on, was found only after the user's command had been saved aside and replaced and main switched. All three are asked first now, in both states, so a run that is going to stop leaves the launcher, the checkout and ~/.local/bin as they were.
Co-Authored-By: Codex XHigh <noreply@openai.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Hermes is only ever installed through the app. Choosing it as the default agent used to stand aside for a hermes that worked but came from somewhere else, and to refuse one that predated seeded sessions; both left the machine on a Hermes that was not the app's, which is the one Omarchy prepares for in-app updates and hands the theme to. Now --check answers only for the app's own Hermes, and --now installs the app whatever answered to hermes before, saving the previous command aside as it always did for the app path. The desktop installer no longer adds the package itself, since the shared install does.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The re-point of a user plugin link stranded by the elsewhen package moving from plugins/ to shell/plugins/ landed in migration 1789581661, which every machine updated between that migration shipping and the package moving had already applied. omarchy-migrate keys completion on the filename alone, so the repair never ran where it was needed. On a dev checkout the shell discovers the packaged plugin only through that link, and a rescan drops a dangling one, so Elsewhen vanished from the bar on the first shell start after the package upgrade.
Renaming the file runs the whole migration again everywhere. Each step is safe to repeat: the package add is a no-op once installed, both link branches are guarded, the rescan is best-effort, and the put keeps an existing placement. The new test holds a state directory with only the old marker and checks the renamed file is still pending, which the old name could not pass.
Whether the environment mise built is gone was read by searching the global listing for the prefix `"pipx:hermes-agent`, so a tool that merely starts the same way, `pipx:hermes-agent-tools` say, read as the retired one still requested. Removing the real one changed nothing, the recheck failed again, and the migration stayed pending on every update. The key is matched whole now, with or without its options.
Co-Authored-By: Codex XHigh <noreply@openai.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The wait before clearing a stale shallow.lock looked for a git whose command line began with `git`, so one started as `/usr/bin/git`, which is what a caller that resolved it with `which` runs, was never seen: its lock, once a minute old, would have been cleared under it and a second fetch started against the same shallow file. The path is allowed only ahead of the name, at the start of the command line, so a process that merely names git in an argument, an editor opened on /usr/bin/git from inside the runtime say, is not waited for.
Co-Authored-By: Codex XHigh <noreply@openai.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Upstream's installer runs `npx playwright install chromium` for the browser tools, and npx asks before fetching a package it does not have. Over ssh, with no terminal, it goes ahead; in the floating terminal the menu opens for choosing Hermes it printed "Ok to proceed? (y)" and waited, so an install a user had every reason to walk away from sat there until someone typed y. The mise-built Hermes this replaces never asked anything. `--skip-setup` already answers the wizard the same way, and the app's own bootstrap never had a terminal to ask in.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The 2026-09-19 revision of this migration, and a symlink then shipped under config/, pointed every install's ~/.config/omarchy/plugins/omacom.elsewhen at /usr/share/omarchy/plugins/omacom.elsewhen. elsewhen 1.0.0-2 moved to shell/plugins, so the link dangles, the shell lists the plugin as enabled but never loads it, and the bar carries an empty slot. The migration kept any existing link, resolving or not, so a rerun could not repair it. A link into /usr/share/omarchy that no longer resolves is Omarchy's own and is re-pointed; a link the user made is still left alone.
A bare `omarchy-shell shell rescanPlugins` exits 1 when no shell answers on the caller's tree, and omarchy-migrate runs migrations under `set -e`, so an update run from a TTY or over ssh, or one whose shell had gone down while the package replaced its QML, died at "omarchy-shell is not running" before its post-update hooks, shell restart and reboot prompt, and re-failed the same way every time until a shell could be asked. The rescan is now best-effort, like `omarchy-bar put` already is and like #11117 makes the plugin commands' closing rescan: the update restarts the shell once the migrations are through, and a session without one has no bar to place on. An unknown widget on a live shell still leaves the migration pending.
Choosing Hermes as the default agent built it through mise: a pipx environment with no checkout, so `hermes update` had nothing to move, and the only Hermes that could update itself was the one Hermes Desktop set up. Both paths now run the same setup. omarchy-install-hermes-cli installs the hermes-desktop package and runs upstream's installer from it, pinned to the packaged release and started on main, exactly as Install > AI did; omarchy-install-ai-hermes is that plus opening the app. The terminal, the default agent and the app share one runtime, and it updates itself.
--check answers whether --now has anything left to do, not merely whether a hermes runs: choosing Hermes from the menu asks first and opens a terminal only on a no, so a yes has to mean no minutes-long step would run where nobody can see it. With the app installed that means the runtime's own command, its completion marker and the seeded packaged app; a finished runtime whose command is gone, somebody else's, or its own but unable to run gets it back from upstream's path stage without bootstrapping again. Either way the command has to be the one PATH finds, because omarchy-agent runs bare `hermes` and Omarchy puts mise's shims ahead of ~/.local/bin; a command in the way is named rather than installed over. The modes are named outright because the app's launcher used to call this command with no arguments to reconcile a mise copy; a default of --now would turn every launch into an install. --check still refuses to run the retired wrapper, since running it built Hermes through mise, and a machine whose migration is pending can still have it on PATH.
Provisioning no longer writes the wrapper, Remove Preinstalls no longer looks for it, and the wrapper, the environment it built and what proves them Omarchy's are known to the installer alone: --retire-mise is the migration's whole job, and --now runs the same removal once the runtime installer has saved the wrapper aside, so a user who chose Hermes before their migration ran is not left with mise's shim answering `hermes`. Only the wrapper proves the environment is Omarchy's, at its path or in that saved copy, so the environment goes first and the wrapper last, judged by mise neither having it installed nor still requesting it; a removal that leaves either behind, or a listing that cannot be read, mise missing included, stops with the commands to finish by hand and leaves the migration pending. The migration that once installed the wrapper is kept as a no-op for late updaters, and one whose default agent was Hermes is told to choose it again.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>
Keep inhibitor state in validated private directories and verify the recorded owner, PID, start time and launch token before signaling. Authenticate the held command before detaching and drop it back to the invoking user.
Serialize launch and cancellation, identify the child before publishing its state, and preserve caller-owned idle choices. Cover cross-account fallback state, process identity, cancellation, retry, and update-lock handling with isolated regressions.
Steam depends on the virtual package lib32-vulkan-driver. omarchy-pkg-add
runs pacman with --noconfirm, which silently resolves that to the first
provider, lib32-nvidia-utils, pulling the NVIDIA stack onto machines with
no NVIDIA GPU. Running omarchy-install-gaming-gpu-lib32 first satisfies
the dependency with the driver that matches the detected GPU.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
idle.screensaver / idle.lock of 0 was included in Math.min(), so
IdleMonitor's timeout became 0. Releasing a Wayland idle inhibitor
(SDL's default screensaver inhibit) then reported idle immediately and
locked the session even when lock had been "disabled" by setting it to 0.
Treat 0 as disabled when computing the first idle deadline, do not start
the corresponding action, and leave IdleMonitor off when both timeouts
are 0.
Fixes#10860
Restarting immediately after the plugin rescan races Quickshell IPC handler creation and can crash the exiting shell. The normal update flow already restarts after migrations. Let this migration finish through live enablement and placement without adding timing workarounds.
Link the package into the existing plugin directory and let bar put handle enablement, clock-relative placement, and the missing-clock fallback. Preserve user checkouts and existing placements, and seed the same link for new users. This removes the Atreyu packaged-discovery prerequisite and the checkout cleanup and JSON rewrite machinery.
The shell drops its own player: BackgroundMedia is image-only, and the
lock loads Owe.LockFeedSurface through a Loader, so a system without the
module shows no lock video instead of losing the whole lock screen. The
feed pauses per output when the panel blanks or power saver turns on.
The lock view keeps its still effect path and darkens the feed for
legibility. QtMultimedia and the shell video pause policy are gone, and
the base package list requires owe and owe-lockfeed instead.
The desktop background no longer plays videos. OWE owns video
backgrounds, and the shell layer stays empty behind one. The shell keeps
stills, which OWE hands back to it.
Remove the desktop video pause plumbing that only existed to stop an
unseen player: the lock, idle, and battery service lookups, the
per-output fullscreen check, the first-screen audio opt-in, and the audio
output in BackgroundVideo. The lock screen keeps its own silent playback.
Update the background tests, the manual, and the package note.
The background plugin now watches for the OWE daemon socket. While OWE is
running, the desktop yields video playback to it and the shell keeps
stills. The lock screen keeps its own playback.
This lets Omarchy cooperate with OWE without OWE editing shell.json, so
the engine can ship as a package.
Add a package-list note that owe-wallpaper-engine must be added once it is
packaged.
The mode check returned 1 without any output, so a launch from the app
menu died after the polkit prompt with nothing to diagnose. Name each
source directory and the mode it was left with.
Retire only checkouts whose refs and reflogs are reachable from recorded origin history, and preserve ignored files. Leave configs without an explicit supported bar layout untouched so migration does not replace the shell fallback with an almost empty bar.
Co-Authored-By: Codex XHigh <noreply@openai.com>
Elsewhen (omacom.elsewhen) arrives as the elsewhen package under
/usr/share/omarchy/plugins, the packaged root the shell scans between its
bundled plugins and the user's. It opens the right section of the default
bar, just before the tray, and a migration installs the package, writes the
widget into a customized shell.json in the same spot, and retires a pristine
pre-package clone of the upstream repo that the package now shadows.
A numeric chmod keeps setuid/setgid on a directory, so a setgid ~/Windows
stayed 2700 and failed the exact 700 privacy check, aborting the launch.
dockur marks an initially empty /shared setgid (chmod 2777) on first boot,
so this hits fresh installs the next time the bind anchors are recreated.
The settings package's pre-transaction hook runs this command under
pacman's transaction. Its lock wait was unbounded, so a stalled grant
operation could hang pacman indefinitely before AbortOnFail ever saw a
result. Grant operations are short; wait at most 60 seconds.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
pacman continues a transaction after a failed scriptlet. Model that in
the shared package-scriptlet contract: a failed pre_upgrade followed by
post_upgrade must keep the removal blocker while a rule remains, a
clean retry recovers, a stranded blocker with a live rule is cleaned by
the next completed installation, a fresh install that cannot remove a
leftover rule leaves publication refused, and the sweep does not depend
on inherited glob state.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The protected entrypoints revoked the sudo timestamp before installing
their cleanup traps, so a signal or failure during that first sudo -k
exited without the cleanup path. Install the traps first.
The shell restart probed the notification bus name with busctl's
default 25 second timeout, so an unresponsive user bus could stall the
restart by that much per probe. Bound each probe to one second.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Restarting the shell waited for the notification bus name to reappear
before it would re-acquire a lock whose client had died. A slow or
failed notification plugin then left the user stranded behind
Hyprland's failsafe even though the lock service worked.
Wait for the shell's core IPC, re-secure the lock immediately, and only
then wait for a previously running notification service, reporting it
separately if it never returns. Cover the never-returning case: the lock
comes back and the restart still reports the missing service.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Three review findings on the update-hook boundary:
The pre-refresh-pacman hook had been moved after the refresh transaction
and, during a channel switch, deferred to the very end. That defeated the
hook's purpose: custom repositories and IgnorePkg entries were not in
place when the downgrade-capable -Syyuu ran. Run the hook where it used
to run, after the package config is re-synced and before the transaction,
but cold: revoke the timestamp, run it behind the no-update wrapper with
the caller's original PATH, and revoke again before continuing. Every
later privileged command authenticates with --no-update, so a detached
child left by the hook has no reusable timestamp to wait for. Channel
switching hands the caller's PATH to the refresh the same way the updater
receives it, and no longer defers or re-runs the hook.
Stay Awake was released before AUR builds, hooks and mise, so the machine
could sleep during the longest part of an update. Releasing the inhibitor
needs no privilege because the held command already dropped to the user,
so stop it after mise and before the reboot prompt, as before.
A packaged channel destination cannot be inspected before its package is
installed, and a transaction can replace the running tree with a release
that predates the command-scoped wrapper; from then on a bare sudo would
resolve to /usr/bin/sudo and publish a timestamp, and the destination's
own updater authenticates the same way. The switch used to abort only
after the packages had changed, with generic rerun advice. Now it checks
for the wrapper after each transaction before any further privileged
step, completes what it safely can, and stops cold with instructions to
run that release's update from a fresh session instead of launching it.
Boundary tests pin the hook between the config copies and the transaction
with a cold timestamp on both sides, the older-destination stop with its
guidance and no launched updater, the new inhibitor position, and the
post-update hook staying unreached on failures and signals. Docs, the
manual and the sample hook describe the restored timing.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Bring in the legacy-grant classifier fix and the reserved-prefix
quarantine from #9457 so this branch no longer carries a stale copy of
that command.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Matching a legacy grant by its filename and rule relationship is not a
complete fingerprint: the legacy writer took the filename from $USER but
produced the rule with echo, and under BASH_ENV with xpg_echo a name such
as ali\0143e yields an alice rule in a mismatched file. Preserving that
as administrator policy let the migration certify success with an
unrestricted grant still live until the next boot.
The prefix is reserved anyway: boot cleanup and the package hook remove
everything under it. Move any file the classifier does not recognize
into a fresh root-only directory under /var/lib/omarchy/sudoers-quarantine/
as `policy`, with the original name stored beside it, so nothing there
stays live, the administrator keeps the content, and a legacy filename
already close to NAME_MAX still fits. An untrusted quarantine directory
keeps the migration pending. Cover the mismatched and maximum-length
legacy files in the unit cleanup and through the real migration runner.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The legacy command wrote the caller's unvalidated name into both the
sudoers filename and the rule. Cleanup applied the current lower-case
account pattern to that suffix, so an exact legacy grant for an account
such as Alice was classified as administrator policy, left active, and
the machine-wide migration marker was written anyway.
Match a legacy grant by its exact filename and rule relationship instead
of the account policy, and cover it in the lifecycle suite through both
the unit cleanup and the real migration runner.
The account pattern itself stays lower-case: sudoers reads an upper-case
word such as ALICE as a User_Alias reference, and ALL as every user, so
such names must never reach the generated rule. Pin that with a test.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Upstream now runs every Omarchy-owned pacman transaction through the
hidden omarchy-update-pacman helper so a mid-transaction systemd reexec
cannot kill it. Keep the deferred pre-refresh-pacman hook and the
command-scoped sudo wrapper, and call the helper from the refresh and
channel commands; the wrapper still applies to the helper's own sudo.
The sudo boundary fixture copies the helper into its root and runs a
systemd-run stand-in that execs the wrapped pacman step in place.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Remove unsafe project bin PATH injection
* Cover customized unsafe Mise paths
* Revoke legacy Mise Work trust
* Harden legacy Mise trust cleanup
* Preserve ignored Mise Work configs
* Scope Mise path cleanup to env
* Accept paranoid Mise ignore marker
Reported-by: infosec-us-team
* Keep screen-recording state out of world-writable /tmp
* Compare the /tmp name across the run instead of requiring it absent
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Fall back to the state directory when there is no runtime dir
* Let the /tmp snapshot come back empty
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Resolve the region file the same way in the resizer
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Protect recording fallback state and document its path
---------
Co-authored-by: Omabot <omabot@omarchy.org>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
publicPluginManifest() stripped __sourceDir from every non-first-party
manifest, but service and panel plugin instances receive only that
stripped manifest and no separate sourceDir (unlike barWidgets, which
get meta.sourceDir). That left third-party service/panel plugins with
no way to locate their own bundled scripts and assets: any plugin that
launches a bundled script resolves it against an empty base path and
fails silently (e.g. Omarchy-Spotify hangs on "Checking playback
support…", its keyring/credential scripts never run).
__sourceDir is the plugin's own install directory, not a privilege
signal — the plugin's code already runs from there. Keep withholding
the real privilege markers (__isFirstParty, __hostCapabilities) but
stop stripping __sourceDir. The plugin auth-boundary test suite still
passes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
No branch on the repo is protected and there are no rulesets, so the
file never enforced owner review. Its only effect was auto-requesting a
review from the other owner on every PR, which nobody acted on.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The repo moved to the omacom org. GitHub redirects the old URLs, but
`omarchy channel set dev` was still cloning from basecamp/omarchy, which
left every dev checkout with a stale origin remote that confuses gh
(pr create fails with "No commits between omacom:quattro and
basecamp:<branch>"). Update the clone URL, the quattro upgrade tarball,
the update-confirm release link, the systemd Documentation link, and
the manual.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSFKDatumRZHB8zqk5CP5C
1Password 8.12 changed its app id from "1Password" to
"com.onepassword.OnePassword" (its .desktop file now declares that as
StartupWMClass). Our window rule only matched the old name, so the main
window came up tiled and lost the no_screen_share protection.
Match both forms so older installs keep working.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSFKDatumRZHB8zqk5CP5C
A floating window on the console is not laid out by the gaps, so it no
longer stretches the panel to full width. Moving an app onto or off the
scratchpad and toggling floating now refit too, via
window.move_to_workspace and window.update_rules; both were measured on
Hyprland 0.56.2 to carry the settled count.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012UyVoFTM98Tduoxg7qZax7
Cubic keeps pushing until packets drop, which stands queues up in the path
on fast links. BBR paces to its estimate of bottleneck bandwidth and minimum
RTT instead, cutting queueing latency while keeping throughput. fq is the
qdisc BBR is built to pace through.
tcp_bbr and sch_fq are modules in every kernel Omarchy ships and autoload
when the sysctls are set. The migration re-applies the shipped file so new
connections switch without a reboot, no-ops once the live values match, and
flags a reboot if applying fails.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Offline installs unpack the bundled tarball and pin Node to its exact
version, since latest can't be resolved without network. But nothing ever
loosened that pin, so Node stayed frozen at the ISO's version and mup
skipped it forever, while online installs tracked latest.
Rewrite the pin to latest right after registering the bundled version:
mise resolves latest to the installed version while offline (verified
with no network and an empty cache), and the first mise up with network
picks up new releases just like an online install.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017LseZ1jcLaFBnndRnW4yb5
udev patterns are shell globs, so nvme[0-9]*n[0-9]* also matched every
partition. Partitions have no queue/scheduler, and udev logged a "Could not
chase sysfs attribute" for each one at boot. Restrict the match to
SUBSYSTEM block with DEVTYPE disk, which also keeps mmcblk boot areas and
NVMe multipath nodes out.
Reword the comment: kyber targets a read latency rather than bounding it,
and the kernel's default choice depends on the device rather than being a
fixed NVMe versus everything-else split.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The kernel leaves NVMe on none and everything else on mq-deadline. Neither
bounds latency once the device queue fills, so a large build, copy, or
package upgrade can make the desktop sluggish while reads wait behind a
wall of writes.
Kyber keeps separate read and sync-write queues and throttles the depth it
submits to hit a 2ms read target, which keeps interactive reads flowing
under heavy writes at negligible CPU cost. The trade is a small ceiling on
peak throughput on very fast devices, which matters for a storage server
chasing IOPS but not for a desktop.
Ships as a package-owned udev rule in /etc so it applies at boot and on
hot-plug. Existing installs pick it up at the next boot. zram is left alone
since it has nothing to schedule.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The update conflict tests stub sudo and pacman, but omarchy-update-pacman
now puts systemd-run between them, so on a systemd-booted host the tests
would reach for the real system manager. Stub systemd-run to drop the
wrapper's options and run the command, and cover the helper's own
invocation composition in a new test.
Raised by codex review.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015zcqENR1UbhuwC1v5u3Wop
omarchy-channel-set runs under set -e, so a failure after it has begun
mutating the system (dev link, pacman channel, packages) died silently
with the switch half-applied. Trap ERR once the mutation phase starts
and say how to pick the switch back up.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The floating-terminal presentation wrapper showed the green "Done!"
prompt for every exit code except Ctrl-C, so a failed update or channel
switch closed looking like a success. Pass the command's exit code
through to omarchy-show-done and render a red "Failed (exit code N)!"
prompt when it is non-zero. The pkg install/remove pickers get the same
treatment.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Upgrading systemd runs its post_upgrade scriptlet mid-transaction, which
reexecs both the system manager and every user manager. When pacman runs
inside a user-session scope (the floating update terminal), that reexec
can SIGKILL it and abandon the transaction halfway, with packages
upgraded but none of the post-transaction hooks run.
Route every Omarchy-owned system mutation through a new hidden
omarchy-update-pacman helper that registers the transaction as a PID 1
scope via systemd-run, keeping it out of the user manager's cgroups.
System scopes survive the system manager's own reexec, and as a bonus the
transaction now also survives its terminal window closing. On unbooted
systems (the installer chroot) the helper runs pacman directly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The theme laid out the logo, password entry and progress bar once, when
the script first ran. Plymouth sizes its window to the largest display
present, so a monitor that comes up after plymouthd starts (a Thunderbolt
display whose DisplayPort tunnel is established about a second after the
kernel's display driver loads, a dock, an MST hub) got the prompt drawn at
the internal panel's coordinates: off-center on the external display and
partly off-screen on the panel. With the lid closed the passphrase box was
effectively invisible.
Move the layout into a function and re-run it from the refresh callback
whenever Window.GetWidth/GetHeight change, which is how Plymouth's script
plugin exposes a display being added or removed. Existing bullets are
repositioned too.
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
The battery service ran `powerprofilesctl get` every two seconds to keep the
active profile visible to the wallpaper and lock services. That command is a
PyGObject script, so the shell spawned a Python interpreter for it tens of
thousands of times a day. Roughly once a day one of those exits into a CPython
3.14 finalization race (python/cpython#124619): the GLib D-Bus worker thread
calls PyGILState_Ensure after the interpreter is torn down and the process
dies with SIGSEGV, leaving a core dump and a crash notification behind.
Read the ActiveProfile property straight from power-profiles-daemon with
busctl, the same way omarchy-powerprofiles-set already reads UPower. The
output is JSON, so an empty or malformed reply when the daemon is not running
still reads as no active profile, matching the previous behaviour.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011gbfh4Mi9dK6SAd1P2xMTi
The speaker's USB firmware stops answering control requests when the host
stops the audio stream after WirePlumber's 5 s idle suspend. The kernel
then logs usb_set_interface failed (-110), clock source 1 is not valid,
and cannot set freq 48000 err -110; PipeWire fails to start the sink and
only a replug recovers it. On one machine this happened on six days over
three weeks, up to hundreds of timeouts a day.
A WirePlumber rule sets session.suspend-timeout-seconds = 0 for the KEF
node only, so the stream is never stopped and the trigger never fires.
Other sinks keep the default. The migration seeds the file for existing
installs and restarts WirePlumber if it is running, since conf.d is only
read at startup.
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Electron rewrites ~/.config/Claude for as long as the app runs, so removing the package and deleting the directory while it is open leaves the app running and the directory back within the same second, holding fresh Preferences and crash-reporter state. Driven end to end on an edge worker: remove via the menu with the app open left 11 processes and a recreated ~/.config/Claude; with the app quit first the directory stayed gone. The test stubs pkill so the suite cannot take a developer's own Claude with it, and asserts the remover reached for it.
Co-Authored-By: Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>
Follows the ChatGPT flow: the Install > AI entry runs
omarchy-install-ai-claude in a floating terminal, which installs the
claude-desktop package (Anthropic's Linux desktop beta, repacked from
their Debian repo in omarchy-pkgs) and opens the app. Remove > AI
drops the package along with ~/.config/Claude and ~/.cache/Claude,
the Electron directories the desktop app owns, while keeping
~/.claude, ~/.claude.json, and ~/.cache/claude-cli-nodejs: those
belong to the Claude Code CLI, which ships in its own package and
survives this removal, just as the ChatGPT remover keeps the Codex
CLI.
The menu mark is a new U+E90E glyph in the Omarchy icon font, from
Simple Icons' Claude mark, so it reaches desktops through the next
omarchy-settings release.
Co-Authored-By: Fable 5 <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>
Mirrors the hey-cli stub: the wrapper in ~/.local/bin installs and
upgrades through mise on first run, so the CLI tracks releases instead
of going stale as a manually dropped binary.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Chainfire (@Chainfire) privately reported the passwordless asdcontrol watchdog reboot on August 25, 2026.
The fix landed in the 4.0.2 security work: #9200 (df819a6f) removed Omarchy's unrestricted sudo grant, while omacom/omarchy-pkgs#242 and omacom/omarchy-pkgs#243 restricted the package's grant. This records the reporter credit omitted from those changes.
Reported-by: Chainfire (https://github.com/Chainfire)
Thanks to _SiCk // afflicted.sh for independently reproducing the passwordless sudo persistence issue and reviewing the proposed fix in #7990. The follow-up fix landed in #9387 (945af75a), with the 4.0.3 backport in cf459ba7.
The original boot-cleanup patch was contributed by @Adolanium. Preserve that co-author credit alongside this acknowledgment.
Co-authored-by: Adolanium <94890352+Adolanium@users.noreply.github.com>
* Relay the Elgato Cam Link 4K as a 16:9 virtual camera
Browser meeting apps such as Zoom's web client ask the Cam Link for a
standard-definition stream, and Chromium settles on the smallest mode it
offers, 640x480. The Cam Link fills that 4:3 frame by cropping its 16:9
input, and the app then paints the frame into a 16:9 tile, so everyone
comes out stretched wide. The web client has no HD switch to avoid it.
Hide the raw capture node from users and re-expose it through v4l2-relayd
as a 1280x720 virtual camera with the same name, so there is still just
one "Cam Link 4K" to pick and no way to negotiate 4:3 from it. udev
starts the relay whenever the Cam Link enumerates and stops it on unplug,
and the relay only pulls frames while something is watching. The sink
runs unsynced because v4l2src stamps each frame with its capture time,
which a synced sink treats as already late and drops.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Take the review fixes for the Cam Link 4K relay
Tie only the device's stop into the relay instance. A start dependency on
it left a job waiting on a device that never comes whenever the base
v4l2-relayd.service is started without a Cam Link attached, since the
package generator wants every configured instance.
Let the loopback unit rerun on each relay start, so a deleted or unloaded
device is recreated on replug instead of the oneshot staying satisfied.
Start the relay outright at the end of the migration. The udev trigger
only starts it when the rule is new to the device, and a failed module
build would otherwise pass silently with the raw camera already hidden.
Run the hardware fix after the Panther Lake kernel swap, as it pulls in a
DKMS module that would otherwise build twice.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
A when: subl guard hid the row before install, which is how the editor
gets packaged. Map stored sublime_text to subl so an existing default
stays ticked, and update the default-apps tests for the PATH name.
Arch removed the prebuilt broadcom-wl package on 2026-09-02 and rebuilt
broadcom-wl-dkms with replaces=(broadcom-wl). A replaces entry only helps
upgrades of an already-installed package, so the name is now unresolvable
as an explicit pacman target and the ISO offline mirror download fails
with "target not found: broadcom-wl".
broadcom-wl-dkms depends on dkms, so the explicit dkms in the hardware fix
is redundant; linux-headers stays because the module has to build against
the running kernel.
The runner already rejects a slash, a bare . or .. The installer still
joined the type into hooks/<type>.d before mkdir/cp, so a name nothing
can run could still land on disk.
1Password reads the display scale itself, the way Electron apps do, so
on a scaled monitor it comes up oversized next to every other window.
Pin it with --force-device-scale-factor=1 and let the compositor scale
it.
The app menu is covered by the packaged .desktop, which we build
ourselves in omarchy-pkgs. This is the other route in: the hotkey runs
the binary directly and never reads that file.
Claude-Session: https://claude.ai/code/session_01JB9phxP56gnP7qSidkkUJE
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clicking the keyboard layout widget switches one device, chosen by
filtering the seat through UNTYPED_KEYBOARDS and then taking whichever
survivor sits furthest through the layout list. That filter has to
recognise every non-keyboard by name, and a laptop registers far more as
a keyboard than it lists.
On a Dell XPS 14, Hyprland reports ten keyboards and one of them is a
keyboard. The filter catches three of the other nine, leaving vendor
hotkey blocks (intel-hid-events, intel-hid-5-button-array,
dell-privacy-driver, dell-wmi-hotkeys) and two HID endpoints ahead of
at-translated-set-2-keyboard, which sorts last. Every click switches
hid-sdw:...-consumer-control instead, so the label cycles convincingly
while typing never changes. Device order is stable across polls, so it
is deterministic rather than a race, and needs no pre-existing bad state.
Switch every keyboard holding the same layout list instead, naming an
absolute index. "next" advances each device from wherever it sits, so a
seat that has already drifted apart stays drifted and merely inverts;
one index converges it in a single click, and a seat in lockstep leaves
the reading nothing to disagree about. Keyboards given their own
kb_layout hold a different list and are left alone, since an index into
this list would not mean the same layout to them.
The evdev KEY bitmap would separate these cleanly - the real keyboard
emits 167 keys, the pseudo-devices at most 19 - but hyprctl devices
reports no capability information, so the switch is taken out from
behind the name filter rather than the filter being lengthened.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Hiding the top bar and removing the window gaps are the two things you
do to give the screen entirely to your windows, and doing both took two
hands and two hotkeys. `omarchy toggle fullscreen desktop` does them
together.
It only leaves full screen when both halves are in it, so hitting the
hotkey with just the bar hidden (or just the gaps gone) pulls the other
half into line instead of flipping the one you already set.
Claude-Session: https://claude.ai/code/session_01JB9phxP56gnP7qSidkkUJE
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
T3 Code reads themes an environment publishes into its state directory, so
a generated t3code.json carries the palette the same way claude.json and
vscode-theme.json already do, and omarchy-theme-set-t3code republishes it
on every theme change for the app to pick up live.
The template overrides only the roles a theme actually designs -- terminal
and code surfaces, text on canvas, borders, selection -- and leaves muted
text, placeholders, and status foregrounds to T3 Code, which contrast-solves
them against whatever canvas it is given. Overriding those directly cost
readability on light themes: White fell to 1.0:1 on placeholder text.
Installing from the menu now routes through omarchy-install-ai-t3-code so a
fresh install publishes the current palette and opens wearing it.
Keep the package launcher visible when Hermes builds register a second desktop entry. Use the existing launcher hide list so in-app updates cannot restore the duplicate menu row.
An open terminal can retain deleted SQLite WAL files across removal and reinstall, causing the updated desktop to refuse session writes. Check user processes before package/runtime removal and again after data confirmation, without killing sessions. Cover the failure with real SQLite writers in isolated fixtures.
Co-Authored-By: GPT-6 Codex (xhigh) <noreply@openai.com>
Align main with the packaged release and fetch connected history so the updater detects and rebuilds the first update. Guard local branch work before upstream installation, force only the admitted incomplete release pin, preserve existing command files, and explain incompatible packages. Exercise empty-directory publication races and delayed launches.
Co-Authored-By: GPT-6 Codex (xhigh) <noreply@openai.com>
Use the packaged upstream installer and matching prebuilt app to prepare the writable user installation before launch. Preserve existing builds and modified sources; record the upstream build stamp only for a matching prebuilt app.
Co-Authored-By: GPT-6 Codex (xhigh) <noreply@openai.com>
* Add Muse Code as a default coding agent
Meta ships Muse Code only as a binary, so it installs from the AUR
(muse-code-bin) instead of mise, and a fresh install runs the muse login
browser flow in the install terminal before the agent opens.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148qKzr366p2Ubu2igCLvPg
* Refine Muse Code menu and prompt forwarding
* Install Muse Code from OPR
* Install Muse Code through mise's HTTP backend
* Preinstall the Muse mise stub
* Use the shared Muse installation flow
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
The repository moved from basecamp/omarchy to omacom/omarchy, but the skills
Omarchy installs into ~/.claude/skills, ~/.codex/skills and ~/.agents/skills
still name the old owner. Most gh calls follow the rename, so nothing looks
wrong in normal use. Search does not: `gh issue list --repo basecamp/omarchy
--search ...` exits 0 with no output, and the raw search API answers
"Validation Failed". An agent following contributing.md or reporting.md runs
its duplicate check before filing, sees zero matches, and files a duplicate.
Replace the owner in contributing.md, reporting.md and the issue-template
contact link, which is the same stale name on the contributor path. The git
clone and release URLs in bin/ and manual/ are left alone: GitHub redirects
those, and channel-test.sh asserts the exact clone URL, so changing them is a
separate change.
Fixes#10118
Written by Claude Fable 5.1 via Claude Code, reviewed by Marc Morriss
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* Add Cursor CLI as a coding agent choice
* Launch Cursor CLI through its agent subcommand with --trust
Cursor CLI still dispatches a one-word prompt that names one of its
subcommands (update, login, help) even after a bare --, so name the agent
subcommand outright and pass the prompt behind -- there, where it also
keeps a prompt starting with a dash from being read as an option. --yolo
only auto-allows commands; the workspace trust dialog is skipped only by
--trust, and a launcher that must not stop to ask needs both.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Leave an official Cursor CLI install alone
Cursor's own installer symlinks ~/.local/bin/cursor-agent, the same path the
mise wrapper takes. The migration now installs the wrapper only when no
cursor-agent command exists, and Remove Preinstalls deletes the path only
when it holds the wrapper omarchy-mise-install wrote, the way the Hermes
wrapper is handled. Selecting the agent treats an executable at that path
other than the wrapper as the user's own install and skips mise, since the
mise shims precede ~/.local/bin on PATH and a mise copy would only shadow
it. The wrapper resolves through mise's registry, which lists cursor-agent
from 2026.8.15 on.
The tests write a real cursor-agent stub before Remove Preinstalls runs,
cover the preinstall opt-out for the new migration, and check that a
symlinked official install survives removal and selection alike while a
dead file at the same path still installs.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Give Cursor its brand mark and one name in the menu
Add Cursor's mark to the Omarchy icon font as U+E90D and point the agent
entry and both editor entries at it, so one brand is drawn one way across
the menu. Label the agent entry "Cursor CLI", the name the command and the
manual already use, and spell it the same in the migration and the tests.
Append the manual row after the others at the standard width.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Keep an official Cursor CLI install through a user re-provision
User setup writes every mise wrapper unconditionally, which is fine on a
fresh install but replaces the symlink Cursor's own installer leaves at the
same path when omarchy-provision-user runs again with --force. Guard that
one line the way the migration does.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: John Cavanaugh <59479+cavanaug@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* Add native video wallpaper support
* Pause video wallpapers while a fullscreen app is focused
* Sample one frame when a video background sets the bar text colour
A video wallpaper made the transparent bar's colour sampling decode the entire file. ImageMagick's video delegate runs ffmpeg with no frame limit, so a twenty-second 1080p background took 11.3s of CPU where one frame takes 0.14s, and it did that on every theme change.
The result was unusable anyway: a multi-frame input emits one value per frame, which the single-value match then rejected, so transparent bars silently fell back to the plain text colour on every video wallpaper. Selecting frame zero fixes the cost and the colour together, and fixes animated GIFs, which had the same bug.
Co-Authored-By: Codex XHigh <noreply@anthropic.com>
* Load wallpaper video lazily, and without an audio output
Three costs the still-image path should never have paid.
BackgroundMedia imported QtMultimedia at file scope and was instantiated on every output, so the module and its audio dependency closure mapped into every shell process whether or not a video was ever shown — measured at +2.72 MiB RSS. Moving the element into its own file behind a Loader that takes a URL defers the whole import: an inactive loader maps none of it, an active one maps all 25 libraries. An inline Component cannot defer that, because the type has to resolve when the file compiles.
Qt's Video convenience type always builds an AudioOutput, and `muted` only aliases that sink's volume, so every monitor decoded an audio stream it would never play and opened an audio client for it. A bare MediaPlayer with no audio output spawns no QFFmpeg::AudioR, QAudioContext or PWDevMon thread, and plays files with no audio track just the same.
The shared image also turned mipmapping on, which the desktop background never had. A full mip chain is about a third more texture memory — 10.6 MiB extra at 4K, per output — for a wallpaper drawn at its own size.
Co-Authored-By: Codex XHigh <noreply@anthropic.com>
* Stop wallpaper playback while the session is locked or screensaved
Playback stopped only for a focused fullscreen window. Locking the session did not stop it, and the lock screen starts a player of its own, so an N-monitor desktop reached 2N decode pipelines the moment it locked — and stayed there, because a display blanked for idle stops being presented but does not stop Qt's FFmpeg engine, which drives its own clock. A laptop locked with the lid shut decoded video until the battery ran out.
The lock and idle services already know both states, so the background service takes the shell reference the loader offers it and reads them. Looking a service up by id needs the registry to be reactive, or a background that loads before the lock service would bind to null and stay there.
Co-Authored-By: Codex XHigh <noreply@anthropic.com>
* Fan out video thumbnails narrower than single-threaded image jobs
The generator fans out one job per core, which was bounded because VIPS_CONCURRENCY=1 made each of them single-threaded. ffmpegthumbnailer leaves FFmpeg's automatic decoder threading on, so a folder of uncached videos put a codec thread pool on every core at once. Queueing video work separately keeps the still-image path at full width and gives the video path a quarter of it.
* Recognize a named video file as a theme preview
The backgrounds fallback beside it already picks videos, so a theme shipping preview.mp4 was the one case that still went unseen.
* Document video backgrounds in the manual
The manual described backgrounds as images only. Worth saying plainly that a video wallpaper costs far more power than a still one and that each monitor decodes its own copy, since neither is visible from the picker.
* Stop the lock screen's own playback once the displays go dark
Pausing the desktop wallpaper on lock only moved the cost. The lock screen builds a player per monitor of its own, so locking an N-monitor session went from N decoders to N rather than to none — and the lock service blanks the displays five seconds later without touching them, which is where a lock spends nearly all of its time. A laptop locked and shut still decoded video into a dark panel.
The service already owns both transitions, so it records whether the displays are dark and the lock view stops playback while they are. The manual said playback stops while the screen is locked, which was the same overstatement; it now says once a locked screen has gone dark.
Co-Authored-By: Codex XHigh <noreply@anthropic.com>
* Keep videos out of the lazy thumbnail path
A lazy row stands in with the media file itself until its thumbnail exists, and the picker draws that with an Image — which shows a picture and shows nothing for a video, with no reload once the real thumbnail lands. So the first open after discovering an uncached video showed a blank tile.
The same branch also spawns one generator per file immediately, before either queue is reached, and the theme switcher always asks for lazy thumbnails. That put the narrower video fan out on the one path that never used it: forty uncached previews meant forty ffmpegthumbnailer processes. Sending videos to the queue instead fixes the blank tile and puts them back under the cap.
Co-Authored-By: Codex XHigh <noreply@anthropic.com>
* Rebuild the theme preview cache after teaching it about video
Preview discovery changed what it recognizes, but its cache keys on theme directory mtimes alone. A theme that already shipped a video preview would keep whatever the old rules cached until something happened to touch the directory. Bumping the version rebuilds it once.
Co-Authored-By: Codex XHigh <noreply@anthropic.com>
* Drop an activeAudioTrack setting that never took effect
Qt's FFmpeg backend ignores setActiveTrack while no source is open, and the literal binding is not reapplied once the media loads and the tracks become known, so the line did nothing. What actually keeps the audio decoder and its client from ever being built is the absent audio output, which a file carrying an audio track confirms on its own: no QFFmpeg::AudioR, QAudioContext or PWDevMon thread appears without it.
Co-Authored-By: Codex XHigh <noreply@anthropic.com>
* Give up the blank state when a display comes back
The lock screen stops its wallpaper while the displays are dark, but it was tracking the blanking it asked for rather than the panels themselves. Opening a docked lid turns the internal panel back on without going through runWake, and so does a resume, which left a visible lock wallpaper frozen on one frame until the next keypress. A frozen wallpaper someone is looking at is worse than the decoding it saves, so a screen change gives the state up.
Co-Authored-By: Codex XHigh <noreply@anthropic.com>
* Time bound the video thumbnail generator
Routing videos through the queue means they are generated before the picker opens rather than behind it, which turned an unreadable or stalled file into a picker that never opens. ffmpegthumbnailer had no bound of its own and the drain waits for every job. A generator that gives up is already handled: the run reports failure, the partial file is removed, and the row drops out of the list.
Co-Authored-By: Codex XHigh <noreply@anthropic.com>
* Pause only the output a fullscreen window covers
The fullscreen test was global, so a game on one monitor stopped the wallpaper on every other one — including the ones still in plain view. That is the failure the lock work was careful to avoid, and it made the manual's claim that playback stops when nothing can see it untrue for the commonest multi-monitor case. A lock or a screensaver does cover every output, so those stay a single decision; fullscreen is now matched against the focused monitor, the way the bar already routes by output.
Co-Authored-By: Codex XHigh <noreply@anthropic.com>
* Kill a video thumbnail generator that ignores the timeout
Plain timeout sends TERM and then waits for a process that may never take it, which leaves the bound it was added for unenforced on exactly the stuck files it was meant to catch.
Co-Authored-By: Codex XHigh <noreply@anthropic.com>
* Pause video wallpapers in battery power-saver
* Fix paused video wallpaper source priming
* Skip snapshots for video background transitions
(cherry picked from commit 6f759538bfa76c2da03634e98ebfc2ebf63ec68e)
* Generate thumbnails for direct-scan videos
(cherry picked from commit 10fcca018a865dca311fb6863e8c8b0057291223)
* Remember a video the thumbnail converter rejected
A permanently unreadable video cost ten seconds of generator time on every
picker open before its row dropped, because nothing recorded the failure.
Both the menu image generator and the direct picker scan now leave a marker
beside the missing thumbnail, keyed like the thumbnail on the file's size
and mtime, so a repaired file starts clean. A timeout is left to retry, as
it may only have been a busy machine.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Follow the panels' real DPMS state under a locked video wallpaper
The lock screen stopped video playback when it asked for the displays to
blank, and resumed on input, but never checked what the panels did. A blank
that failed left a lit panel on one frozen frame, and a resume that turned
the same outputs back on played nothing until the next keypress.
Quickshell exposes no DPMS signal, so while a video is the locked wallpaper
the lock polls hyprctl and decides per surface from the answer. A wake or
blank request drops the last answer so its optimistic state applies until
the next poll confirms it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Pause a video wallpaper for the fullscreen window that covers it
The fullscreen check read the globally active window and the focused
monitor, so it only knew about the window that had focus. A fullscreen
window left on one monitor while focus moved to another resumed the
wallpaper decoding behind it, and with fullscreen windows on two outputs
only the focused one paused.
Each output's visible workspace reports whether a fullscreen window covers
it, and Quickshell flips that on the compositor's fullscreen event, so each
panel now decides from its own monitor's active workspace instead.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Reopen a video wallpaper a theme switch replaced behind its path
Two themes that both ship backgrounds/wallpaper.mp4 leave the current
background at the same path after a switch, so the displayed path never
changed and the running player kept decoding the old file from its open
descriptor. Stills go through the snapshot transition and survive this;
a video switch is instant and did not.
A forced switch onto the path already on show now bumps a reload counter,
and BackgroundMedia rebuilds the video player for it. A cache-busting query
is not an option there, since FFmpeg reads it as part of the filename.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Keep picker rows uncached while a rejected video is left out
Skipping a video with a failure marker let the picker cache its rows
without it, and cached rows are trusted on the directory's mtime alone.
A file repaired in place never touches that, so the marker's fresh key
was never consulted and the video stayed missing.
The generator now hands the marker back to the row loop, which drops the
row and leaves the rows uncached, so each open re-stats the file and a
repaired one is converted again.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Hand each background loader only its own kind of file
BackgroundMedia fed one URL to both the still loader and the video player.
On a switch from image to video the Image was handed the video's URL in
the moment before its loader unloaded, so Qt tried to decode the mp4 as a
picture and logged an unsupported format on every such switch; the reverse
handed the player a still to demux.
The still URL is now empty whenever the path is a video and the video URL
empty whenever it is a still, so a switch changes only the loader that
stays.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Stop a video wallpaper before tearing its player down
Switching from a video to a still destroys the BackgroundVideo item while
its player is mid-read, which FFmpeg reports as a failed open in the shell
journal on every such switch. Stopping the player on destruction lets the
demuxer wind down first, and the switch is quiet.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Play a video wallpaper's sound track from the first monitor
Video wallpapers were always silent: the player was built without an
audio output, since a muted output still decodes the track and opens an
audio client on every monitor. A video with music should be able to play
it.
The player now builds its AudioOutput only once the media reports a sound
track, so a silent file still opens no audio client, and only the first
screen's panel opts in, so a multi-monitor desktop does not layer copies of
the track. The output is muted while a paused player primes its first
frame, and the lock screen stays silent.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Keep a departing video player off the still's file
The switch away from a video still logged a cancelled open, and stopping
the player on destruction only hid it: stopping reports the media as
loaded, which the loaded handler answered by playing again. The real cause
was one evaluation pass. Both URLs derived from the `video` flag, which is
itself bound to the path, and QML updates the two in no fixed order, so
the video URL could evaluate against the stale flag and hand the player
the still for a moment. Its destructor then cancelled that open.
Each URL now tests the path directly, the source binding only applies
while the path is a video and restores nothing when it stops, and the
destruction stop goes away with the hazard it introduced.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Pin the audio wiring in the test and name the output in the manual
The audio assertion passed with the BackgroundMedia forwarding binding
removed, which would have left every wallpaper silent, and did not pin the
silent default or the first-screen selection. It covers all three now.
The manual said the sound track plays "from your first monitor", which
reads as routing to that monitor's audio device. It is the first monitor's
wallpaper that plays, through the default output.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Omabot <omabot@omarchy.org>
Co-authored-by: Codex XHigh <noreply@anthropic.com>
Co-authored-by: z8 <yam@kernelius.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
userdel rewrites /etc/subuid and /etc/subgid, and like every shadow-utils database write it leaves the previous contents behind in a dash-suffixed backup. The scrub removed four of the six backups those tools produce, so the retained @factory baseline still named the previous owner in /etc/subuid- and /etc/subgid- along with their subordinate ID range.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
test/shell.d/bin-style-test.sh has been failing on quattro: the OpenClaw
remover checks for gum with a raw `command -v`, which the style rule
reserves for the helpers themselves.
The Hermes remover does the same thing. It was invisible because the test
fails on the first offending file, so fixing OpenClaw alone just moves the
failure along.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013p9Qh6dX4FuwBJAWhNPVbn
* Use updated libfprint-git for fingerprint setup on edge
* Pick the fingerprint driver from the reader, not the release channel
The channel gate blocked every edge and dev user until the newer
libfprint-git pin is published, misrouted dev checkouts on the stable
mirror, and left the stock-libfprint migration reverting the driver on
accounts without its marker. Key both the setup and the migration on
omarchy-hw-fingerprint-git, a USB ID table of readers stock libfprint
cannot drive, so the git snapshot only goes where it is needed on any
channel. Qualify the package with the omarchy repo, and skip pacman
entirely when the packages are already current so a rerun cannot become
a partial upgrade.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Install libfprint-git for every fingerprint reader
Stock libfprint lags upstream on new readers, and gating the git
snapshot per reader or per channel only added machinery to keep in sync
with the package repo. Install libfprint-git unconditionally instead:
the omarchy-pkgs pin is the single place a new reader gets enabled. The
migration that swapped it back to stock goes away with the policy it
enforced; late updaters keep the driver they have and pick up the new
pin as a normal package upgrade.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: powderluv <powderluv@powderluv.org>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Hermes' YAML reader breaks lines on carriage return, NEL and the Unicode line and paragraph separators, and stops at NUL, none of which grep treats as a line end, so a comment line carrying one could put a root-level key such as banner_logo past the validator and into Rich markup on Hermes' terminal surfaces. The lines grep accepted also did not add up to the YAML Hermes needs: a colour before colors:, a second colors:, or a key over YAML's simple-key limit all passed and loaded as no palette at all, which Hermes shows as its default. The validator now counts every byte outside printable ASCII first, then walks the file in order: the name, at most one plain description, colors:, and only #rrggbb colour lines after it.
omarchy-theme-set releases its lock before the hooks run, so the rendered skin can change under this one between the check and the copy. The check is made on a private copy and that copy is what gets published, both on the first pass and on the republish a minute after activation, which used to copy whatever the theme had become by then, unchecked.
A theme switch reads the config of the profile named in active_profile, which is the one Hermes reads, and a profile exists to Hermes once its directory does, with or without a config; it ends early only for a config plainly naming another skin, since only the default is ever replaced, and leaves anything Hermes might read as the default for Hermes to answer. Hermes is run by the path the readiness probe vets, ~/.local/bin/hermes, bounded the way the probe bounds it; an answer that did not come is not taken for the default, and a write Hermes refuses is reported rather than failed, being cosmetic.
A profile that cannot take the skin no longer costs the others or the activation; a directory at the skin's path is an error rather than a place mv puts the temp file; a temp file the copy could not fill is removed. Remove stops the unit the installer left waiting, so a removal within the waiter's half hour does not hand the theme to a Hermes installed some other way or recreate the skin under a home the user asked to delete. The migration no longer swallows the hook's exit: what is not ready or refused is reported and done with inside the hook, so only Omarchy's own failures return, and those keep the migration pending as the guide requires.
Comments are cut to what the code cannot say; the reasoning is here.
Co-Authored-By: Codex XHigh <noreply@openai.com>
Hermes Desktop installed under Install > AI kept its own palette while every other agent app retinted with the theme. Hermes' skin is its one theme unit for the desktop app, the TUI and the CLI, and its gateway watches the active skin file and broadcasts changes to every surface, so Omarchy publishes a skin named omarchy from a template on every theme switch and nothing Omarchy-specific goes upstream.
Activation goes through hermes config set, which writes the active profile's config and touches the skin so a running gateway repaints at once, and it only replaces Hermes' default skin so a choice made in Hermes stays. A theme switch runs that activation too when the desktop package is present and Hermes is still on its default, so a hand-over the installer missed is finished by the next switch; once the config names the skin a switch never starts Hermes. The desktop adopts a skin from a change broadcast rather than from the config it finds at connect time, and its first launch builds the runtime over minutes, so the installer starts --wait as a transient user unit that outlives the install terminal, activates once the runtime marker appears, republishes after the gateway is up, and reports to the journal. A migration hands the skin to existing Hermes Desktop installs through --activate, which also renders the skin for a theme applied before the template existed.
The generated file is validated before it is published, because Hermes parses it as YAML: only the name, a plain description and #rrggbb colours pass, so an unresolved palette key or a cloned theme's own hermes.yaml leaves the previous skin in place.
🤖 Generated by Fable 5.1 in Claude Code. Reviewed by Fable 5.1 code-review at high.
Perplexity 26.9.1 keeps the secret vault and device identity in
~/.local/state/perplexity and its runtime downloads in
~/.local/share/perplexity-rpc-server; the remover knew neither path, so
it deleted the logins unconditionally while leaving the credentials
behind. Now the runtime and Electron caches always go, and the logins,
vault, and launcher flags go only on an explicit yes at a terminal,
default no, the same choice Remove OpenClaw puts in front of the user.
The prompt needs stderr on the terminal too: gum draws it there, so a
redirected stderr means keeping the data, not blocking on a question
nobody can see.
Co-Authored-By: Codex XHigh <noreply@openai.com>
Co-Authored-By: Sonnet 5 <noreply@anthropic.com>
Keep the migrations themselves for late-updaters. Drop the tests that only
exercised frozen file rewrites from 4.0.0, and keep live invariants,
privileged repairs, and the migrator.
Follows the T3 Code / Grok Bot flow: the Install > AI entry runs
omarchy-install-and-launch, so picking it installs the perplexity
package on demand and launches the app when the install finishes.
Remove > AI drops the package along with the app's own config, flags
file, and rpc-server runtime cache, keeping the perplexity-* caches
that belong to Perplexity's other products. Like the Hermes remover,
it sets -u so an unset HOME is a refusal rather than rm -rf paths
rooted at /.
The menu mark is a new U+E90B glyph in the Omarchy icon font, so it
reaches desktops through the next omarchy-settings release.
Co-Authored-By: Fable 5 <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>
OpenClaw's desktop experience on Linux is its Control UI, served by the
gateway the openclaw package runs, so the Install > AI entry installs
the package and a web app launcher that routes through the new
omarchy-launch-openclaw: first launch hands off to OpenClaw's own
onboarding wizard, later launches start the gateway when needed and open
the dashboard's single-use browser handoff URL as an app window.
Remove > AI tears the gateway service down through OpenClaw's own
gateway uninstall (falling back to systemctl by hand), aborts rather
than dropping the package under a gateway that will not stop, and keeps
the user's agent in ~/.openclaw.
OpenClaw also joins Setup > Defaults > Agent through the same
agent_installer seam Hermes carries: its CLI is the pacman package
rather than a mise tool, so omarchy-install-openclaw-cli answers
--check/--now with pacman, and omarchy-agent runs `openclaw chat`,
seeding prompts through --message.
The menu mark is a new U+E90C glyph traced from the package's lobster
favicon; E90B stays free for the Perplexity mark still in flight on its
own branch.
The launcher recovers the gateway through `openclaw gateway install --force`
(unit not enabled: missing, or an install that died after writing it) or
`openclaw gateway start` (enabled but stopped), never `openclaw dashboard
--yes`: as of OpenClaw 2026.9.1 that defers to "the owning supervisor" in
both cases, and once the gateway is up it copies a one-time browser pairing
URL into the clipboard. The dashboard probe is bounded so an app-grid launch
cannot hang without a terminal to interrupt it. Removal treats only
systemd's own "inactive"/"failed" as a stopped gateway, so an unreachable
user manager aborts instead of dropping the package under a live process.
All of it verified against a real 2026.9.1 install.
Removal also takes down the node-host unit if OpenClaw ever installed one, and
asks (default no, only on a terminal) whether ~/.openclaw should go too, with
its size: the chats and credentials live there next to hundreds of megabytes
of plugin runtimes and cache OpenClaw downloads for itself.
Onboarding goes through omarchy-openclaw-onboard rather than bare `openclaw
onboard`: as of 2026.9.1 the bare command is the guided flow, which ends by
running a foreground gateway and handing off to a browser tab without
returning, so the install script never reached the app launch and no service
was installed. The helper runs the classic wizard (--flow quickstart
--install-daemon --skip-ui) as a background job that keeps the terminal as its
stdin, so its prompts render and take input as upstream draws them, and stops
it once the gateway answers: upstream leaves the wizard running after its
outro (only the TUI branch exits, and the model sign-in holds a socket open).
Every quickstart prompt precedes the service install, so that point is safe.
A gateway that never comes up after this run applies setup ends the wait as a
failure instead
of hanging, an already-running OpenClaw is left alone rather than mistaken for
this run's success, a gateway answering on the port is only this run's once its
process is the unit's own MainPID (an orphan from an
earlier run) is not mistaken for the service this run installs, and a signal at
the helper takes the wizard down with it.
Brave Origin keeps its profile under ~/.config/BraveSoftware/Brave-Origin
rather than Brave-Browser, so the Copy URL and Download Video installers
never wrote their host manifests there. The extensions loaded but the
shortcuts did nothing. Add the Origin profile roots and rerun both
installers through a migration.
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Keep the target user's local bin on user-scoped upgrade paths while giving privileged lock and firewall helpers only root-owned search directories. Pin the lock helper's root PATH and fprintd-list executable, with regression coverage for each defense independently.
Reported-by: Rooke Poole <rookpool97@gmail.com>
foot only applies scrollback.multiplier to discrete wheel clicks, so
precise touchpad scrolling ignores it and crawls at the group's 1.5
factor. Split foot out at 2.0 to match how the other terminals feel.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Address review feedback on the polkit faillock migration:
- Match only the exact stack the setup commands wrote and replace just the bare
pam_unix lines, preserving comments and the hardware-auth lines. An
administrator-authored polkit-1 carrying any other directive is left
untouched, rather than rebuilt from scratch.
- Also repair the markerless post-removal layout: both remove commands strip
their own marker lines but leave the bare pam_unix stack behind, so keying on
a hardware-auth marker skipped those machines permanently.
- Exit non-zero when the backup cannot be created or the rewrite cannot be
verified (after restoring). omarchy-migrate runs under set -e and records a
migration complete unconditionally after it returns, so a failed repair must
fail loudly to be retried instead of silently marked done.
- Add test/shell.d/security-polkit-migration-test.sh covering the fingerprint,
FIDO2, combined, and markerless layouts, comment preservation, idempotence, an
untouched administrator stack, and the refused-sudo and unverifiable-write
failure paths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The fingerprint and FIDO2 setup commands create /etc/pam.d/polkit-1 from
scratch on Arch, where the polkit package ships its stack in
/usr/lib/pam.d/polkit-1 and /etc/pam.d/polkit-1 does not exist. The
hand-rolled stack listed pam_unix directly instead of including system-auth,
which dropped pam_faillock from the polkit path: polkit prompts had no
brute-force lockout, their failures were not recorded, and they did not count
toward the lockout protecting login and sudo. Defer to system-auth, matching
the vendor file and the sudo stack, keeping the clamshell gate and the
pam_fprintd / pam_u2f sufficient lines in front.
Add a migration to repair installs the old setup already configured, since the
forward fix does not rewrite an existing polkit-1. It acts only on an
Omarchy-created polkit-1 that lacks the system-auth include and carries a
hardware-auth marker, preserves the configured auth lines, backs up the
original, and is idempotent.
Add a test asserting the stack each setup creates defers to system-auth; the
created polkit content was previously untested.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A failed --remove told the user to run it again, but that advice could
never work: rm -f has usually taken the marked stub by the time the
failure is judged, and a rerun that finds nothing it owns succeeds
without touching the mise environment it was asked to finish removing.
Spell out the three commands that complete the job by hand instead.
Also correct the story the probe comment told: chat never lost
--oneshot in v0.20 -- no released Hermes defined it there. It lived at
the top level until v0.21 added chat's own, so the old probe was keyed
to a flag no release ever carried under chat, and every install read
as not ready. Recorded straight so a future hermes-desktop bump to
v0.21+, which would make the old probe pass on the desktop path alone,
cannot read as the fix.
Findings from omarchybot's review (Opus 5, with Codex at xhigh).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
* Honor keepLoaded for services during plugin hot-reload
Plugin reload destroyed every service, including omarchy.lock, which drops the ext-session-lock client while Hyprland still holds the lock and surfaces the crashed-lockscreen fallback.
* Prove keepLoaded service survival with a fixture service
A fresh lock service also reports an empty lastEventAt, so comparing it
across the rescan passed whether or not the instance survived. A fixture
keepLoaded service whose in-memory marker is set before the rescan and
read back after can only pass when the same instance is still mounted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Drop kept services whose plugin no longer declares a service
The _syncServices cleanup only asked whether the plugin was still
installed and enabled, so a kept service whose plugin dropped its
service kind or entry point kept running as a zombie until shell
restart. Apply the same eligibility checks used at creation, and hand
kept instances the refreshed manifest after a rescan.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Cover omarchy.media in keepLoaded expectations; note kept services reload on restart
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>
Two review follow-ups. The probe counted any mention of --tui/--query in
the help as support -- Hermes already writes "With --tui:" into --dev's
description, so a release that dropped the option while keeping the
prose would still read as ready. A flag now counts only when followed by
a shape argparse prints after a definition: the usage bracket, the gap
before same-line help text, an uppercase metavar, or the line end. Not
probed by parsing a real invocation on purpose -- a release that ignores
unknown arguments would turn the probe into a live session.
And the teardown trusted its commands: a stub rm that failed aborted
Remove Hermes under set -e before any ~/.hermes handling, while mise
failures vanished into || true. --remove now attempts every step, then
judges by what is left -- the marked stub still present, or mise still
resolving the tool -- and Remove Hermes tolerates the failure until the
runtime is handled, then carries it in its exit code.
Findings from the same codex review at xhigh, verified and proven by
mutation before landing.
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude <noreply@anthropic.com>
The teardown removed the Omarchy tool spec from mise unconditionally,
so removing Hermes Desktop could destroy a mise environment the user
had built against the same spec while sparing their wrapper -- the very
command the removal claims to preserve, broken behind its back. The
whole teardown now turns on the marked stub, as replacement already
does; the desktop takeover keeps its own bargain, where a second Hermes
goes whoever built it and the app still provides the command after.
Also close the probe over underscores -- _ continues a flag name just
as - does, so --tui_mode no longer answers for --tui -- and pin the
gaps review found in the tests: each flag must match on its own (either
grep could be deleted before without a failure), a foreign wrapper's
mise environment must survive --remove, and Remove Hermes must tear
down the CLI in the interrupted-install case, not only after the app's
runtime landed.
Findings from an independent codex review at xhigh, each verified
against the source and proven by mutation before landing.
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Remove Hermes dropped the desktop package and its ~/.hermes runtime but
never touched the mise CLI, on the assumption the install-time handoff
had already removed it. A CLI the app never superseded -- an interrupted
install, or the terminal CLI from before the app existed -- was left
stranded on PATH after uninstall.
Add a --remove mode to omarchy-install-hermes-cli that performs the same
teardown the desktop takeover already does (mise rm -g + mise uninstall,
and the marked stub), and call it from omarchy-remove-ai-hermes. The tool
spec and ownership marker stay defined in one place, so the takeover and
teardown paths cannot drift. Scoped to what Omarchy owns: a Hermes the
user installed themselves is left alone.
Co-Authored-By: Claude <noreply@anthropic.com>
Review follow-up on the readiness probe. The two greps were fixed-string
substring matches, so a future release listing only --tui-theme or
--query-log while dropping the bare --tui/--query omarchy-agent passes
would read as ready -- the same false verdict inverted. Anchor both to a
flag boundary.
Add a regression case pinning that a substring-only help is rejected,
and one exercising --check through a mise-installed hermes in both
capability directions: the desktop and foreign cases only covered their
own wrappers, and the mise path is what a machine without the app runs.
Co-Authored-By: Claude <noreply@anthropic.com>
omarchy-hook and omarchy-state set join a name straight into a path. A name
with a slash, or a bare . or .., points outside the hooks or state directory.
Every caller in the repo passes a fixed label, so this is a footgun guard for
future callers, not a fix for anything that ships today.
Names with dots in the middle (a..b) stay allowed. omarchy-state clear is
untouched: find -name matches basenames only.
Hermes v0.20 removed chat's --oneshot flag, which hermes_prompt_ready
used as its capability marker. A fully bootstrapped Hermes Desktop
install then read as not ready: --check failed forever, the default
agent flow looped back into the installer, and --now dead-ended with
"Launch Hermes Desktop once to finish installing it" on a machine
where it already had.
Probe for --tui and --query instead: the flags omarchy-agent actually
passes to seed an interactive session, rather than one that merely
shipped alongside them.
Remove stale passwordless sudo grants during boot and revoke a live grant immediately if its transient expiry timer cannot be created. Exercise both failure paths and the shipped tmpfiles rule against a disposable root.
Co-authored-by: Adolanium <94890352+Adolanium@users.noreply.github.com>
The old setup command enabled sshd before importing a key, so an aborted
run left a password-only server exposed. Skipping that machine kept the
hole Omarchy opened; close it instead by disabling sshd. Omarchy is a
desktop distro, so the console remains, and the warning explains how to
set up key-based access or deliberately re-enable password logins.
With the stakes flipped from skip to disable, "no usable key" must not
false-positive: follow an authorized_keys symlink to its key (dotfiles
setups have working key auth), and treat an unreadable file as
unverifiable rather than keyless.
Amends the unreleased 1788124236 migration in place; no released install
has run it, so every machine still gets the new behavior in one pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Validate authorized_keys line by line with the question sshd actually
asks: ssh-keygen -lf on the whole file also fingerprints a private key
copied there by mistake, which sshd cannot use, so the migration would
have disabled the only working login path.
Tighten ~/.ssh and authorized_keys the way omarchy-setup-security-sshd
does, and back off from a group-writable home directory: StrictModes
makes sshd ignore the key either way, with the same lockout.
Complete with a notice instead of failing on conditions the migration
cannot repair (a broken or pre-Include sshd_config, an overriding admin
rule, a failed reload of a valid config), so those machines keep passwords
as they were without blocking every migration queued behind this one.
Only missing privileges stay pending, since a terminal rerun fixes that.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Without a terminal sudo keys its cached credential on the parent
process of each call, so the timestamp validated by sudo -S -v in the
test shell never reached omarchy-setup-security-sshd's own sudo calls
when omarchy-iso-test drove the suite over ssh with no pty, and the
exercise died with 'a terminal is required'. Run it under script(1)
and validate the password on that pseudo-terminal first, so every sudo
underneath shares the terminal-keyed credential.
The missing-checker case dropped $ROOT/bin from PATH to make
omarchy-git-url-check unfindable, but installed machines carry the
packaged checker in /usr/bin, so it was always found and the test
failed on every 4.x machine. Shadow it with a stub that reports
command-not-found so the scenario holds regardless of the host.
The mount-boundary test tmpfs-mounts over /home before sourcing
$ROOT/bin/omarchy-windows-vm, so a checkout living under /home vanished
mid-test and set -e aborted with no output. Take a mount-safe copy of
the helper into the test tmpdir before the mounts land.
The suite verifies the finished product: VM runs never use a dev-linked
tree, so the session-environment lookup and own-checkout fallback were
needless indirection. /usr/share/omarchy is the default; a caller testing
a different tree passes OMARCHY_PATH itself.
Assert the closed session-to-root paths on an installed system — no blanket
input-group membership, no shipped asdcontrol sudoers grant — and exercise
omarchy-setup-security-sshd unattended end to end: sshd up, key authorized,
password and keyboard-interactive authentication off in the effective
config, SSH port rate limited in the firewall.
The sshd section mutates the machine, so it requires the explicit
OMARCHY_ACCEPTANCE_SUDO_PASSWORD opt-in that omarchy-iso-test passes for
its throwaway VMs; elsewhere it skips.
Tesseract routinely drops small caption text at native resolution — the
weather panel's detail labels fail the WIND assertion with the text plainly
on screen. Let the compositor upscale the capture instead.
Run over SSH with no OMARCHY_PATH, the acceptance runner defaulted it to
its own root — wrong in both sync modes omarchy-iso-test uses. With only
test/ synced, the root has no shell or install manifests: omarchy-shell
refuses every call and the package audit passes vacuously against an empty
manifest. With a full tree synced, the path disagrees with the config path
the session shell was started from, and since qs matches instances by that
path, every omarchy-shell call reads as "not running".
The suite acts on the running session, so ask the user manager for the
session's own OMARCHY_PATH first, then fall back to this checkout, then to
the installed tree.
The shell notices the bar-off flag through a FileView watch on the toggles
directory, and that watch can permanently stop delivering events after flag
changes land in quick succession — the bar then stays parked off screen
until the shell restarts. Have omarchy-toggle-bar nudge the bar's probe
over IPC after flipping the flag, so the toggle no longer depends on the
watch staying alive. The watch remains for other writers of the flag.
The Style submenu grew its Unlock entry back (d411c90a) the same day the
menu acceptance test was written, so the blind Down-key walk landed on
Font and picked a font instead of opening the Menu Bar submenu — the bar
position assertion then timed out on every run.
OpenSSH 10.x prints configuration keywords in CamelCase in its sshd -T
dump, where 9.x printed them lowercase. The case-sensitive grep in
omarchy-setup-security-sshd therefore never matched on OpenSSH 10.x, so
the hardening drop-in was always judged ineffective and removed, leaving
password authentication enabled.
pacman-rc.conf shipped with [omarchy] on pkgs.omarchy.org/edge — a
leftover from when release candidates published there. Candidates now
publish to a dedicated rc channel, so a machine switched to rc with
omarchy-refresh-pacman was pairing the rc Arch mirror with edge omarchy
packages, and omarchy-version-channel could not name the rc repository
at all (an rc install reported 'rc / unknown').
Point the conf at pkgs.omarchy.org/rc, teach omarchy-version-channel
the rc repository, and repoint existing rc-channel machines with a
migration. The migration only rewrites the shipped pairing (rc mirror +
edge [omarchy]); an administrator's deliberate combination is kept.
Apply the Omabot patch on Quattro, verify effective SSH hardening, prevent stored provisioning state from restoring the blanket input-group grant, and stop Omarchy from shipping asdcontrol authorization that belongs to the package.
Co-authored-by: David Heinemeier Hansson <david@hey.com>
The scratchpad spanned the whole screen whatever was on it, which reads as a band rather than a console when it holds the single agent it is usually opened for. It now sizes to a centered panel twice as wide as it is tall, and hands the full width back as soon as a second app is on the scratchpad: two windows splitting a half-width column is worse than the band this replaced.
The panel is still drawn with workspace gaps rather than a window rule, because Hyprland resolves a rule's size once as the window maps and the console has to survive a rescale. It is always flush with the top and always centered, so two numbers describe it, and the memo that suppresses no-op rewrites compares those rather than four edges.
The window count is read on window.open and window.destroy, the two events that run after the workspace's count has settled; window.close and window.move_to_workspace both still count the window on its way out, so refitting from either reads one too many and strands the console at full width. The count is only chased while the console is on screen, since a hidden one is refitted on its way in by workspace.special_active, and every window opened anywhere on the desktop would otherwise rewrite the rule.
Monitor width and height are the panel's own pixels, so an output turned on its side still reports them the way the panel is built rather than the way it hangs. The odd transforms are the quarter turns and those swap the work area: without that a rotated 1920x1080 sized its console from 1920x1080 instead of 1080x1920 and left a 214x1367 sliver.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>
* 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>
Catches the branch up on 94 commits so what lands here is reviewed against
current quattro, and so #8611 contributes its own five files rather than
dragging a partial catch-up in behind it.
bin/omarchy scans the first 80 lines of every command for omarchy:* metadata. The requires-sudo flag is surfaced via 'omarchy commands --json', and the interactive launcher and menu use it to flag entries that need elevation before the user clicks them.
Every install-service script in bin/ has the directive: spotify, signal, 1password, sunshine, dropbox-remove, nordvpn, once, tailscale, plus the full omarchy-install-* and omarchy-apply-* trees. omarchy-remove-service-dropbox has it.
omarchy-install-service-dropbox is the outlier. The menu hints 'needs sudo' for removing Dropbox but not for installing it. The install path runs pacman via omarchy-pkg-add, which needs elevation. The missing directive is an oversight, not a deliberate choice.
The HUD is a frameless Electron window that paints its own per-pixel
transparency. Under the default rules it gets a compositor border and Omarchy's
window opacity on top, which turns the compact prompt into an outlined, muddy
canvas.
Scoped to the HUD by title, so the main Hermes window keeps the ordinary
treatment.
Co-authored-by: Luiz Filipe <moresco.luiz@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
`>|` 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>
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>
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.
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.
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.
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.
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.
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.
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.
The provision-user suite never ran the one-shot migration. Cover default-home
links, a pre-existing profile, idempotency, and a missing skill source.
Document ~/.gemini/config/skills and stop wrapping that bullet.
Hermes was missing from the provision-user symlink list that already
covers Claude, Codex, Pi, Antigravity, and ~/.agents. Add ~/.hermes/skills
plus existing ~/.hermes/profiles/*/skills. Migration for current installs.
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
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.
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>
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>
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
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>
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>
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>
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>
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>
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>
Hermes now keeps chat queries interactive and literal to TUI control syntax, so launch it directly and let its own session flow replace the local one-shot, usage-file, and resume bridge.
Gate installation on the capability added with native interactive queries, preserve unowned mise environments, and keep the unprompted launch path unchanged.
Co-Authored-By: Codex XHigh <noreply@openai.com>
Clear inherited session-source tags for Omarchy's local one-shot process so Hermes records the launch directory before the exact session is resumed in the TUI.
Co-Authored-By: Codex XHigh <noreply@openai.com>
Bind option-looking prompts to one-shot mode, require a successful completed usage report before resuming, replay the prompt after first-run setup, and reject Hermes runtimes that lack the session-report capability.
Co-Authored-By: Codex XHigh <noreply@openai.com>
Keep the one-shot session on Hermes' native CLI source so it records the launch directory before the exact session is resumed in the TUI.
Co-Authored-By: Codex XHigh <noreply@openai.com>
Hermes TUI startup queries execute slash, shell, interpolation, and multiline syntax before reaching the model. Run the prompt through literal one-shot mode, read its exact session ID from a private usage report, and resume that session in the TUI so arbitrary prompt text stays data while the conversation remains interactive.
Co-Authored-By: Codex XHigh <noreply@openai.com>
Hermes oneshot deliberately exits after answering, which closes the agent terminal. Seed the TUI chat session instead, keep inherited flags after the subcommand for older Hermes parsers, and bind the query as one argument so dash-prefixed prompts remain data.
Co-Authored-By: Codex XHigh <noreply@openai.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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.
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.
The agent table lists every CLI Omarchy pre-wires, and Hermes was missing from
it. Hermes Desktop earns a paragraph of its own under the graphical apps,
because the one-Hermes-per-machine arrangement is something a user meets rather
than reads about: the app installs its own runtime on first launch, the terminal
command and the default agent then use that same one, and removing the app takes
the runtime but keeps their chats, memories and skills.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
U+E90A carried a trace of the Hermes app icon: a portrait whose detail collapses
into a grey smudge beside the ten flat silhouettes the rest of the font is made
of, which is what icon-font.md warns against when it says to pick a source whose
silhouette alone reads.
It is Font Awesome's staff-snake now, under CC BY 4.0 -- the mark Hermes serves
as its favicon and titles its README with. The README records that, along with
the licence the artwork carries, since it is the only note of where these come
from.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three files spelled out the line that marks ~/.local/bin/hermes as Omarchy's:
the installer that writes it, Remove Preinstalls, and the migration. Two of
them were copies, and a change to what ownership means would have left them
matching a line nobody writes any more -- Remove Preinstalls quietly sweeping
nothing, the migration mistaking Omarchy's own wrapper for a stranger's.
omarchy-install-hermes-cli --owns answers it now, and the other two ask. The
installer's own metadata was also a flag behind: --check has been there since
this landed and was never listed.
A test pins the marker to one file, so a second copy fails rather than drifts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
desktop_hermes_ready decided from a marker file and a text match, while a
hermes the user installed themselves had to answer --version before it counted.
The marker says the app's install once landed, not that it is still there, so a
runtime deleted afterwards left --check reporting success: the default agent
records Hermes, skips the install terminal, and the launch fails.
It now runs the command, on the same 15 second budget the app itself uses. The
path match is a plain string for the same reason it is in the remover -- the
dot in ~/.hermes would otherwise claim a wrapper pointing at ~/xhermes.
foreign_hermes_runs never tested foreignness, only that the command runs, so it
is hermes_runs now and both callers share it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
Remove > AI > Hermes deleted ~/.hermes/hermes-agent, bootstrap-cache, bin and
node unconditionally, plus any wrapper on PATH pointing into ~/.hermes. The
official Hermes installer uses those same paths, so a user who installed the
CLI themselves, then installed the app and never launched it, lost their
checkout, venv and any local changes -- while being told their chats, memories
and skills were safe.
The app provisions its runtime on first launch and writes
.hermes-bootstrap-complete when it lands. Without that marker the app never got
that far and everything under ~/.hermes predates it, so dropping the package is
the whole job.
Two smaller things in the same path. The wrapper test matched ~/.hermes as a
pattern, and the dot made it claim a wrapper pointing at a sibling like
~/xhermes; it is a plain string now, and a symlink there is the user's
arrangement rather than something to delete. And -u, so an unset HOME is an
error instead of a set of rm -rf paths rooted at /.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
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>
The stub exports UV_PYTHON so mise builds Hermes against 3.13, which Hermes
requires and Arch's Python is past. Exported, it survived the exec into Hermes
itself and reached every command the agent shells out to. Hermes is a coding
agent that runs commands in the user's own repositories, so a `uv venv` or
`uv sync` there resolved 3.13 as well: on a project declaring
requires-python >=3.14, uv warns that the interpreter contradicts it and builds
the venv anyway.
Dropping it at the handover keeps the pin over the install, where it belongs.
mise x resolves the tool it already installed without it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
install/user/mise.sh is sourced through run_logged under `bash -eE`, and its
status reaches omarchy-provision-user's `set -euo pipefail`. Every other line
in the file writes a mise stub and cannot fail; omarchy-install-hermes-cli can,
and does whenever hermes-desktop is installed but the app has not been launched
yet -- what a second user on a shared machine meets on their first login.
The rest of provisioning runs after that source: refreshing applications, the
default browser, the mailto handler, the first-install migration markers and
the finalize-user marker. Without the marker the whole step retries and fails
again at every login, and omarchy-provision-first-run calls it with `|| true`,
so nothing surfaces. omarchy-install-ai-hermes and the migration already guard
this call the same way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
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>
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
- 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).
Hermes joins Install > AI as a desktop app, sits beside it under Remove > AI,
and becomes a choice in the default-agent list. The CLI installs through
omarchy-install-hermes-cli rather than a bare `mise use`, so its interpreter
is pinned before mise builds it.
Rebased onto quattro. Ori claimed U+E909 in #7709 while this branch was open,
so the Hermes mark moves to U+E90A in the icon font, the menu entries, the
font README, and the charset the menu test pins. The glyph outline itself is
unchanged; it is spliced in beside Ori rather than over it.
Co-Authored-By: witcheer <witcheer.eth@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SySdB3RtCA8BNv6Am246BP
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>
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
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>
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>
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>
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.
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.
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.
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.
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.
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.
* 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>
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.
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.
* [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>
* 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>
* 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>
* 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>
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>
* 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.
* 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>
* 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>
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>
* 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>
* 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>
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>
* 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>
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>
The refusal table exercised slash, leading dot and leading dash but never a
control character, so that branch of the guard could have been dropped without
a test noticing.
Add a newline and a tab case, labelled so the value stays out of the output.
The wrapper is written through an unquoted heredoc, so the package and bin
names land in it as shell source. A package name carrying shell characters
became code that ran every time the wrapper ran.
The command name is used raw as a file name under ~/.local/bin, so a slash in
it wrote and removed somewhere else entirely.
Quote both values with printf %q, and refuse command names that are not plain
file names before anything is removed or written.
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.
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.
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.
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.
* 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>
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>
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.
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.
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.
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.
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.
`launch_windows` passed the VM password to xfreerdp3 as `/p:"$WIN_PASS"`.
`/proc/<pid>/cmdline` is world-readable, so every other user on the machine
could read the password for as long as the session stayed open — `ps` was
enough.
The arguments now go in over stdin with `/args-from:stdin`, leaving only that
flag in the process list. The argument list itself is unchanged, and building
it as an array also fixes the unquoted `$RDP_SCALE` expansion.
* 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>
* 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>
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>
* 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>
* 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>
The panel handed its three Repeaters snapshots of live PwNode objects, and Quickshell deletes a node the moment PipeWire drops it. Qt then dereferences that row without a null check in VDMListDelegateDataType::createMissingProperties (qqmldmlistaccessordata_p.h:222, unchanged in 6.11.2), so any teardown that emptied the graph with the panel open segfaulted the shell: a USB DAC unplugged, a Bluetooth profile switch, omarchy-restart-audio, a WirePlumber restart, or a laptop suspend.
Rows now carry the node's id and name rather than the node, and each delegate resolves its own PwNode through nodeFor(), so labels, glyphs and stream volumes stay live. The name is what pins a row to the node it came from, since PipeWire reuses object ids; it is CONSTANT on PwNode for as long as the node exists. This is the projection the bluetooth panel already uses against the same crash class.
Filtering the snapshot instead does not work, which is worth recording because it is the obvious thing to reach for: the node is usually alive when the snapshot is taken and dies while Qt is incubating the delegate, so nothing visible at snapshot time identifies the row that will dangle.
Co-Authored-By: Codex XHigh <codex@openai.com>
* 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>
* 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>
* 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>
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>
* 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>
* 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>
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.
mise ships with upgrade.auto_prune on, so `mise up` deletes the old
install directory as soon as a tool upgrades. Long-running processes are
still executing out of that directory, and some re-exec themselves by
absolute path, so running `omarchy update` or `mup` breaks them mid-session.
Claude Code is the case that surfaced it. It records its own binary path
at startup and dispatches its bundled ugrep and bfs through it, so once
the upgrade pruned that path its grep and find wrappers fell back to the
mise wrapper in ~/.local/bin. That wrapper cannot stand in for the real
binary here: a shebang script never sees the argv[0] it was invoked
under, because the kernel hands the interpreter the script path instead.
The dispatch collapsed to plain `claude -G`, which fails.
Keeping the old versions costs some disk and leaves the stale directory
behind until something prunes it deliberately, which is the cheaper half
of the trade.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Use (( )) for the numeric argument test
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Drop the quotes on a variable inside [[ ]]
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Use omarchy-pkg-drop instead of raw pacman -Rns
omarchy-pkg-drop already filters to installed packages, so the
2>/dev/null || true suppression is no longer needed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Drop defensive checks around default-set commands
ttfx, imagemagick, and networkmanager are all in the default package
set, so their commands are runtime invariants and should be invoked
directly. Removing the nmcli guard also removes the degraded wifi
fallthrough that only ran when nmcli was missing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* 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>
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.
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>
* 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>
* 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>
* 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
* 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>
* 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>
* 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>
* 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.
* 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>
* Launch claude and codex agents with auto-review instead of full bypass
Claude's auto permission mode and codex's --approve-for-me both run
unattended without prompting, but keep automatic review (and codex's
workspace-write sandbox) instead of skipping approval entirely. Grok
stays on bypassPermissions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Match the bash aliases to the agent launcher's auto-review modes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The dial already grouped its digits for the locale above 10, but below 10 it
went through toFixed, which hardcodes a dot. A German desktop therefore read
9.5 and 1.235 off the same dial, switching decimal convention halfway up the
scale.
Send both branches through the locale. A reading is a measurement rather than
interface text, so its separators follow the system's number conventions even
though the interface itself stays English.
English output is unchanged.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The calendar grid's header row and the week-start toggle label were the only
text in the shell that followed the system locale, so a German desktop drew
MO DI MI over an interface that is English everywhere else. Nothing chose that;
they were the only two places reading day names off Qt.locale().
Take them from en_US instead. Where the week starts still follows the locale:
that is a regional convention rather than a translation, and it stays
overridable through weekStartDay.
Dropping the trailing-period strip with it, since that existed only for the
locales this no longer renders.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
omarchy-plugin-clone only takes the source id as the first argument, but the
menu passed --edit ahead of it, so the id fell through to the unknown-option
branch and every clone from Setup > Plugins failed.
Closes#6913
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Bar widgets propagate their composed press-and-hold down to the center gesture
area without handing over the grab, so the gesture area started a bar move and
then received neither a release nor a cancel to end it. The move ghost stayed on
screen for the rest of the session. Ignore the gesture unless we hold the press.
Closes#6881
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
mise use -g announces the resolved tool on stdout, so every wrapped command
prepended a "tools:" line to its own output. That corrupts anything speaking a
protocol over stdout, such as codex app-server. Pass --quiet, which keeps errors
on stderr and preserves the exit status.
The obsolete-wrapper check in the agent migration matched the generated command
verbatim, so loosen it to match the package instead of the flags.
Closes#6908
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
A 3.8.3 migration appended the section header with one backslash too many, so
sed wrote the literal characters instead of a newline plus the header. foot
rejects the line and stops reading the rest of the file. The later text-binding
migration matches the header with grep -qxF, misses the broken line, and appends
a second section, leaving the config broken across the Quattro upgrade.
Closes#6903
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
A whitelisted bare git repo over $HOME driven only by constrained
omarchy dots commands: local snapshot history at batch boundaries plus
squash-published state sync across machines. Design survived adversarial
review; rejected alternatives and rationale recorded in the plan.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every reference doc was audited claim-by-claim against the code.
file-layout and omarchy-shell were the most decayed (renamed commands,
the etc/ overrides source split, dead IPC entry points and example keys);
update-process lagged the recent pipeline changes and gains a channels
section; theming and audio-tuning were accurate but thin around their
lifecycles.
New reference docs for the subsystems that had none: the menu system,
the CLI router, the notification daemon, and the non-acceptance test
architecture. AGENTS.md links the two of those agents will need most.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Providers never returned JSON rows: they are shell-defined row sources
emitting tab-delimited lines, and extensions cannot declare new names.
bar.shellQuote moved to Util.qml, and the UpperCamelCase widget id
migration no longer exists.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Task procedure lives in agents/skills/ (migrations.md moves there),
system-shape reference in docs/ (AUDIO-TUNING.md renamed to match),
end-user documentation in manual/. AGENTS.md now states the split.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hyprland reaps its own children, so os.execute() gets ECHILD from waitpid and
never sees an exit status. Every call reported failure, which meant the NVIDIA
env detection in hypr/nvidia.lua never set NVD_BACKEND, LIBVA_DRIVER_NAME or
__GLX_VENDOR_LIBRARY_NAME. Read a marker off stdout instead.
Closes#6914
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* 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>
Install rows hid themselves with `when:"! <present>"`, so software you
already had vanished from the very list it was installed from. Add a
`disabled:` guard that keeps a row listed but dim, ✓-marked, unselectable
and out of search, and move every Install row onto it.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Install missing apps when choosing defaults
* Restore Chromium through browser installer
* Trust default app installer status
* Use full conditionals for install paths
* Host the manual's images in the repo under manual/images
Replaces all learn.omacom.io/manual.omakub.org hotlinks with local webp
files capped at 1600px wide (~20MB total), so the manual is fully
self-contained. Theme and unlock previews are converted from the
canonical themes/*/preview.png files. A handful of illustration shots
are interim conversions of the current images, pending retakes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Retake the illustration screenshots fresh at 3x scaling
Navigation, clipboard history, notices, tmux layouts, prompt, and About
reshot on a clean workspace at 3.13x (1920x1080 logical on 6K) across
ten themes: Tokyo Night, Catppuccin, Gruvbox, Kanagawa, Everforest,
Nord, Osaka Jade, Rose Pine, Matte Black, and Ristretto.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Drop the extra themes chapter from the manual
The community theme gallery will live elsewhere. Removes the chapter
and its 114 gallery images, renumbers the following chapters, and
repoints the two references (theme installs still work via
Install > Style > Theme in the menu).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Reference the shipped theme previews instead of copying them
The themes chapter now points straight at themes/*/preview.png and
preview-unlock.png, so previews can never drift from the source and
manual/images drops from 6.3MB to 3.5MB.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Restore preinstalls from the menu, and drop the Omacom apps with them
Remove Preinstalls missed omacut, omacalc, and omawrite, so the three Omacom
apps survived an opt-out that was supposed to clear the desk.
Opting out was also one-way. Install > Preinstalls now puts everything back:
the shipped .desktop launchers and mise stubs via omarchy-refresh-applications,
the dropped packages via pacman, and the opt-out marker deleted so the
preinstalled keybindings return on reload. The two menu entries guard on the
marker, so exactly one of them is ever visible.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Trim the preinstall lists to what quattro actually ships
Remove Preinstalls was still dropping typora, spotify, 1password, 1password-cli,
signal-desktop, opencode, claude-code, and github-cli. None of those are in
omarchy-base.packages anymore: typora gave way to omawrite, the services moved
to on-demand menu installs, and the agent CLIs are mise-managed. Removing them
took out apps the user had deliberately installed, and restoring them would have
put back what we no longer ship.
Both lists are now the same twelve packages, all of them in omarchy-base.packages.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Keep the opt-out marker when a restore fails
omarchy-pkg-add exits non-zero when pacman cannot install a package, but the
restore ran straight past it, cleared the marker, and reloaded Hyprland. That
reported success and brought back keybindings for apps that never arrived. The
marker now falls last, behind a check on the transaction.
The new test also pins the two lists to each other and to omarchy-base.packages,
which is the drift that let retired packages linger in the removal list.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Pacman answers its own conflict question with No under --noconfirm, so one
retired package can stop every update after it. Which package to drop is a
decision rather than a cleanup, so run the upgrade again with pacman asking
when there is a terminal to answer on, and report instead when -y promised
not to ask.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
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>
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>
The switchers chapter shifted everything from chapter 3 up by one, and
the TOC was still on the old numbers. Add its missing entry too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The network and bluetooth panels stopped launching them; the module
catalogue still said otherwise.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add "Coming From Mac or Windows" as chapter 3: a translation layer for
Cmd/Win instincts rather than a feature tour.
Fill the remaining coverage gaps: fcitx5 input methods, laptop speaker
tuning, touchpad haptics, crash capture in the toggle table, the
installable font list, and the ChatGPT Desktop and Grok Bot entries.
Correct claims that drifted from the source: the network and bluetooth
bar icons no longer launch nmtui/bluetoothctl, monitor scaling steps
rather than cycles, and the display widget's scroll steps brightness.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Rebind ghost Copy URL shortcut registrations to the pinned id
Chromium never hands a suggested shortcut to one extension while
another — even a long-gone one — still holds the registration. Profiles
that first loaded Copy URL before its id was pinned registered
Alt+Shift+L under an id derived from the extension's load path at the
time, so the pinned extension never receives the shortcut and the
keypress does nothing (#6816).
The quattro upgrade tried to repair this against one hardcoded
path-derived id, which only ever matched a single home directory. The
historical ids are unknowable in general — they hash long-gone absolute
paths through whatever symlinks existed then — but the registration
itself names the command, so a migration now rebinds any copy-url
command that points away from the pinned id, unless that id belongs to
an extension that is actually installed or the pinned extension already
holds a binding of its own.
Browsers rewrite Preferences on exit, which reverts any repair made
while one runs, so the migration asks for this user's browser windows to
be closed first — failing and staying pending when there is no terminal
to ask in or the prompt is declined. The backup a repair leaves behind
marks it as attempted but unverified: until a browser-free run confirms
the registration stayed repaired, the migration keeps itself pending
rather than trusting a disk state an open browser may still overwrite.
The upgrade-time repair is dropped: the upgrade already runs migrations,
so the migration is the single implementation.
Fixes#6816
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Pin the WhatsApp Slim extension id
Keyless unpacked extensions get path-derived ids, which go stale if the
load path or packaging ever changes — the same class of bug that broke
the Copy URL shortcut for pre-package installs. Pin the id with a
manifest key like the other bundled extensions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Crash capture stays on by default, but Trigger > Toggle > Crash Capture (or
`omarchy toggle crash-capture`) now turns the watcher off. The toggle writes the
usual flag file and stops the unit for this session; the unit checks the same
flag with ConditionPathExists, so the choice survives a logout without the unit
having to be disabled.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
* 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>
* Keep AppLibrary scans out of login shells
A login shell sources the user's profile, and tools like mise touch
~/.local/share on activation. Quickshell's desktop-entry watcher monitors
that tree, so every scan retriggered another scan: a self-sustaining loop
that pinned ~20% of a core at idle. The scans only use bash builtins,
find, and sort, so they don't need the login environment.
Fixes#6806
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Pin the AppLibrary scans to non-login shells in tests
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Cover the KVM and disk requirements, the resources and credentials the
installer asks for, the launch/stop/status commands and --keep-alive,
what the RDP session carries, and what removal deletes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Persist notification images so history keeps avatars
Persisted popup and history entries stored image/appIcon as URLs into
resources that die with the live notification: Chromium-family senders
(every Omarchy web app, WhatsApp included) pass avatars as files in a
scoped /tmp dir deleted when the notification closes, and raw image-data
hints surface as in-process image:// URLs that die with the server
object. Replaying history then found dead references and hid the icon.
Copy file-backed images into the notification state dir when persisting,
keyed by the entry's file stem, and reference the copies from the JSON.
Blank dead image:// URLs so the card falls back to the app icon. The
copies die with their JSON: superseded-popup deletes, history trims and
clears remove them, and a startup sweep collects copies orphaned by a
restart killing a queued job mid-write.
Hold DND-silenced notifications open until their history write has run,
since untracking tells the sender to delete its avatar file, and carry
replayed on-screen rows over via their persisted copies, since the
replay dismisses their live notifications first.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Coalesce silenced updates and bound image copies through temp files
A replaces_id update lands on a held DND notification without a second
onNotification, so releasing after the first write could persist a stale
snapshot. Re-snapshot when the write completes and write again until the
content is stable, reusing the original file identity.
The image copy reopened the sender-controlled path after checking it, so
a file growing or becoming a FIFO mid-copy defeated the size bound. Read
through head -c under a timeout into a temp file, validate its size, and
rename it into place; the startup sweep clears temp files a killed job
leaves behind.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Neither group has any commands behind it, so both only ever printed
"Unknown Omarchy command" when browsed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add chapters for the top bar and its panels, screenshots and recording,
toggles/idle/screensaver, browsers, shell plugins, and networking, which
collects the Wi-Fi, DNS, firewall, and Tailscale material that was spread
across the FAQ, TUIs, and services chapters.
Fill smaller gaps in the existing chapters: user hooks, autostart, menu
extensions, printing, power profiles, factory reset, drive and user
passwords, passwordless sudo, firmware updates, display text size, and
the LocalSend share menu.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* perf(agents): cut codex usage collector memory with SQL filter and cache
The codex collector scanned every row of opencode.db (1.7 GB, 55k+ rows)
with Python-side json.loads, peaking around 716 MB of RSS on every run
-- including the panel's refreshLimits() call, which passed --limits-only
that the collector silently ignored.
Filter rows in SQL (LIKE gates + json_valid + json_extract authority,
mirroring the old Python filter semantics) so giant blobs are never
parsed, and cache the local stats scan in XDG_CACHE_HOME following the
claude collector's pattern (atomic writes, flock, schemaVersion).
--force rescans, --limits-only and normal mode reuse a fresh cache and
fall back to a full scan when it is missing, stale, or corrupt.
Measured: cold scan 716 MB -> 158 MB peak; warm --limits-only ~85 MB
and ~1.4 s. Output record schema and values are unchanged for the same
data (parity verified against the old filter, including malformed rows).
* Scope the codex scan cache's 15-minute reuse to --limits-only
A no-flag run is the widget's periodic refresh, and refreshIntervalSec is
configurable down to 30 seconds; holding every mode to a 15-minute cache
meant stats could lag far behind the interval the user asked for. Mirror
the claude collector: normal runs reuse a scan for ~20 seconds purely to
dedup concurrent collectors, and only --limits-only, which promises just
fresh limits, may reuse a scan for up to 15 minutes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Invalidate the codex scan cache across day boundaries
The cached stats embed date-dependent fields (todayPrompts,
todayTotalTokens, recentDays), but only the file's age was checked, so a
cache written at 23:58 served yesterday's numbers as "today" for up to
15 minutes past midnight. Stamp the envelope with the scan's local date
and treat any other date as a miss.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Reject codex scan caches with a future mtime
A cache whose mtime is ahead of the clock has a negative age, which the
freshness check accepted forever: setting the clock backwards froze the
stats until real time caught up with the file. Require a non-negative
age before trusting the cache.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Never cache an interrupted opencode scan
A transient lock, schema migration, or corrupted database aborts the
opencode scan mid-flight; the partial numbers still serve the current
run, but persisting them let a single bad read suppress opencode usage
for every cache reader until expiry. The claude collector already skips
its opencode cache write on a database error; do the same here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Make the json_valid guard order explicit in the opencode query
The query relied on json_valid(data) evaluating before json_extract(),
but SQLite does not promise that AND terms run left to right; a
reordered plan would let json_extract raise on a malformed row and
silently truncate the scan. Wrap each json_extract in a CASE so the
guard is structural rather than positional.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Drop a claude-collector comment that is false for codex
"These caches were world-readable before" was copied from the claude
collector; codex had no caches before this one existed. Explain the
chmod on its own terms.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: markbusking <marcosbustos.dev@gmail.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
omarchy-hw-hybrid-gpu gates the Hybrid GPU menu entry, and it queried
supergfxctl unbounded — a wedged supergfxd stalled menu rendering
forever. Bound the query with the same TERM-then-KILL escalation the
toggle uses, and treat a daemon that cannot answer like a machine
without supergfxctl: fall back to counting GPUs rather than hiding
hardware that is really there. An ordinary supergfxctl failure still
hides the entry.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Only offer video capture devices as webcams
V4L2 exposes raw processing and output-only nodes beside usable cameras. The webcam picker and automatic recorder selection treated the first /dev/video node as a camera, so IPU6 laptops opened a black overlay instead of their loopback capture device.\n\nShare one device lister across detection, selection, and recording, and keep only groups whose first video node advertises Video Capture in Device Caps. Cover raw IPU nodes, ordinary capture devices, and capture-less systems.
* Fall through to a later capture-capable node in a webcam group
A group whose first video node is not capture-capable vanished entirely,
even when a later node in the same group could capture. Probe each node
until one qualifies, still emitting at most one device per group. Also
exit zero explicitly: a trailing filtered device used to leak the failed
capability check as the script's exit status.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Bound hybrid GPU mode queries
* Test blocked hybrid GPU queries
* Give the blocked-client test headroom over its 12s of kill cycles
The third case spends ~12s of real TERM/KILL escalation against its own
15s watchdog, which can tip to a spurious 124 on a loaded machine. Also
drop the TEST_LOG plumbing no stub ever wrote.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The interval floor was applied with max(), so the zero that --force
picked could never win: max(0, 15) is 15. Forcing a refresh within
fifteen seconds of the last probe silently served the cache instead,
though --force documents itself as ignoring them.
The window exists to absorb a panel opened and shut repeatedly, which
arrives as --limits-only. --force is a person pressing refresh, and it
should outrank a window meant for flicks.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Stop closed network panels from leaving Wi-Fi scanning enabled
refresh() defaults scanWifi to false and its no-scan branch enabled the
scanner unconditionally. Five paths reach it with no panel on screen —
Component.onCompleted, clearNetworkAction(), failNetworkAction(), the
band-change actionProc exit, and the 30s actionTimeout — so the scanner
stayed on and Quickshell kept re-arming RequestScan behind a closed panel.
scanRestart had the mirror gap: it enabled the scanner 100ms after
refresh(true) without re-checking that the panel was still open.
Every sweep takes the radio off the operating channel, so this degraded
the link it was scanning from: one sweep every 17s, gateway RTT rising
from ~2ms to repeated 150ms+ spikes on an otherwise idle connection.
Gate the scanner block on the panel being open, cancel a pending restart
on close and re-check the panel when it fires, and track the WifiDevice
this instance enabled so close, device replacement and destruction
release the right object. Destruction matters on its own: a bar reload
with the panel open would otherwise die with opened still true and never
write scannerEnabled = false.
* Cover the scanner ownership helper's own invariants
The previous assertion only pinned that no write bypasses
setScannerEnabled(); it said nothing about what the helper does. Dropping
either the opened gate or the release-before-adopt from the helper still
passed, while a closed instance could reclaim scanning and a device swap
could leave the previous interface scanning.
Run the helper's actual JavaScript against stand-in devices instead,
following the extract-and-eval pattern the agents panel tests already use.
Removing either invariant now fails its own assertion.
* Measure the About layout in UTF-8 so its window is not fitted too narrow
wc -L only counts display columns in a UTF-8 locale. A session that never
set one leaves it counting the box-drawing and Nerd Font glyphs the About
layout is built from as nothing, which measured the content 21 columns
narrower than it renders and fitted the window to clip it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Open the About window at the size it last settled on
The window used to map at the float rule's starting size, paint, and only
then measure itself and resize, so every open flashed one window size and
reflowed into another. The size that hugs the content can only be measured
from inside the terminal, so remember it and apply it as a window rule
before the terminal is spawned: the window now maps at its final size and
never moves. A rebranded logo or a new font falls back to the float rule
for one launch, refits, and is remembered from then on.
The fit itself now moves the window by the cells it is off by, rather than
scaling it to the grid, which multiplied up the terminal's padding along
with them and left the fit a column or two short. It accepts a cell of
slack instead of chasing an exact grid, since a window lands where the
terminal's cell boundaries put it, not where it was asked to.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Only the Claude Code CLI can refresh the OAuth token it saves; the
collector just reads it. A machine left alone long enough finds the
token lapsed, and that branch returned an empty limits list with no
status text at all, so the panel hid its whole limits section and
explained nothing.
Say what is wrong, and fall back to the cached limits already on disk
rather than discarding them. Cached windows are kept only until they
reset: a percentage from a window that has rolled over describes a
period that is over, and pinning a stale 78% on an allowance that is
now untouched would be worse than showing nothing. The probe-failure
path gets the same filtering for the same reason.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Drop the kms hook when the proprietary NVIDIA driver handles early KMS
install/hardware/nvidia.sh early-loads nvidia_drm (modeset=1) for early KMS,
but HOOKS still carried the kms hook, so autodetect pulled nouveau and
~100 MB of its GSP firmware into every initramfs for a driver that never
runs. On a Limine UKI setup that meant a 256 MB image where ~144 MB is
normal, doubled again by the fallback history on /boot.
Filter kms out of HOOKS when nvidia_drm is in MODULES (nvidia.conf sorts
before this drop-in) and every PCI display controller is NVIDIA. Hybrid
systems keep kms so the iGPU retains early KMS at the LUKS prompt.
Verified on an RTX 4090 (nvidia-open-dkms 610.57.04): UKI shrinks
256,183,296 -> 144,066,048 bytes, nouveau and its firmware gone, the
nvidia-utils GSP blobs and all four nvidia modules retained, Plymouth
still owns the LUKS prompt via nvidia_drm.
Fixes#6790
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Address review: quote literals, harden PCI detection, add shell tests
Quote fixed string literals in the [[ ]] comparisons per AGENTS.md, and
read the PCI tree through OMARCHY_PCI_DEVICES_PATH, the same seam
bin/omarchy-hw-nvidia already uses.
Require a positively identified NVIDIA display controller before dropping
kms: an empty or unreadable PCI tree previously counted as "no non-NVIDIA
GPU" and would have dropped the hook. Unexpected trees now keep kms.
Cover the conditional in test/shell.d/nvidia-kms-hook-test.sh: nvidia-only,
hybrid, no nvidia_drm, MODULES unset under set -u, audio-function-only,
empty tree, and a device directory missing its sysfs attributes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Treat unreadable PCI devices as inconclusive and make the test hermetic
A device whose class/vendor attributes cannot be read could be another
GPU, so skipping it let a readable NVIDIA GPU beside it drop kms without
having verified the whole tree. Count it as a non-NVIDIA sighting so kms
stays, and cover the mixed case in the test.
The test also sourced the host's /etc/vconsole.conf under set -u, where a
valid KEYMAP-only file makes the XKBLAYOUT expansion fail in the subshell
and ties the result to the machine running it. Predefine XKBLAYOUT and
FILES before sourcing the config.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Rebuild existing initramfses once the kms hook no longer applies
The settings package deploys the new omarchy_hooks.conf conditional, but
nothing rebuilds the initramfs when only a mkinitcpio drop-in changes, so
existing NVIDIA-only installs would carry nouveau's ~100 MB of GSP
firmware until their next kernel update. Following the precedent of
1784476564, add a migration that rebuilds via limine-mkinitcpio — once
per machine, and only where evaluating the installed drop-ins shows the
conditional actually dropped kms, so hybrid machines, non-NVIDIA
machines, and user-edited configs are left alone.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Document the mid-sourcing MODULES caveat in the kms conditional
A later-sorting drop-in that resets MODULES outright (as
surface_device_modules.conf does) would strip nvidia_drm after kms was
already dropped. Every machine Omarchy writes such a file for is hybrid
Intel and keeps kms through the PCI scan, but that is worth stating so
the invariant is not broken by accident.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Quattro hands wifi to NetworkManager, which starts wpa_supplicant through
D-Bus activation. Installs carrying a wpa_supplicant.service mask from the
iwd days break that activation: NetworkManager retries five times, gives
up, and every wifi device sits at "unavailable" with no network to
research the fix on. Remove the mask (including a runtime one) and restart
an active NetworkManager when a wifi device is stuck, so wifi comes back
without a reboot.
Fixes#6783
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The discovery retry timer turned adapter.discovering on every second
while the panel was open, and nothing ever turned it off. The BlueZ
discovery session behind it is held by quickshell's D-Bus connection,
so one visit to the panel left the radio in inquiry until the next
shell restart — continuously starving A2DP audio on the same controller
into stuttering, and 'bluetoothctl show' kept reporting
'Discovering: yes' long after the panel was gone.
The panel now tracks the StopDiscovery it owes BlueZ and settles it
once closed. A timer bound to the confirmed discovery state does the
stopping, rather than a write in the close handler: quickshell only
forwards a discovering write that differs from the last state BlueZ
reported, so a stop issued while a just-fired StartDiscovery is still
awaiting confirmation would be swallowed and leak the session. Binding
to adapter.discovering re-arms the stop whenever the confirmation
lands, a reopen inside the first interval keeps the scan running
uninterrupted, and attempts are bounded so a session another BlueZ
client holds up cannot draw StopDiscovery calls forever.
One widget instance exists per monitor and they all share the default
adapter — the same shared-backend shape the network panel's wifi
scanner fix (#6772) dealt with — so the debt follows the session: an
instance opening onto a running scan adopts it, a closing instance
hands it to a panel still open on another monitor (the popout handoff
closes one instance as it opens the next), and a destroyed instance
passes it to a surviving sibling.
Fixes#6789
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Recover image picker after interrupted thumbnails
* Use arithmetic assertions in image cache tests
* Bound thumbnail lock waits and reap partial thumbnails
A hung generator (vips stuck on a corrupt file or slow mount) held its
flock forever, wedging every later picker open; the directory-lock era
capped that wait at 30 seconds, so keep the same bound. A generator
killed mid-write also stranded its partial .jpg.<pid>.jpg forever, since
nothing prunes the cache directory; only the lock holder writes those,
so reap them right after taking the lock.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Harden thumbnail locks and cache publication against races
Adversarial review caught three holes. The lock fd leaked into
vipsthumbnail, so an orphaned or hung vips kept holding the lock after
its shell died; close it for the child. Reaping legacy lock directories
unconditionally raced a still-running legacy generator through an
upgrade; only reap ones older than the longest plausible generation.
And cache publication was neither atomic nor exclusive, so a picker
killed mid-write, or two interleaving, could leave truncated or
mismatched rows behind signatures that still validated - the same
permanent hiding this branch set out to fix; publish via renames under
a per-key lock, rows first.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix `--help` being skipped for bin routes that only partially resolve to a file
* Add regression tests for --help on partially resolved routes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Stop help-flag scanning at -- and match --json as an exact token
A `--` marks the rest of the args as belonging to the command itself, so
neither --help nor --json past it should be treated as router flags. The
--json check also matched substrings of flattened args, so a --json inside
one quoted argument switched --help output to JSON.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The floating rule pinned every agent terminal to 1200x800, which
overflows small and scaled displays: window rules see logical pixels, so
a 2560x1440 monitor at scale 1.6 is only 1600x900 and the window covered
89% of its height. Tiling drops the fixed size along with the rule, and
the shared app-id still earns the terminal tag from terminals.lua.
* Offer an AI diagnosis when a process crashes
systemd-coredump journals every core dump under a known MESSAGE_ID with the
crashing program, pid, and signal as structured fields. omarchy-crash-watch
follows that stream and raises a "Process crashed: <program>" toast; clicking it
opens omarchy-agent-crash, which briefs the default agent on the crash.
The toast goes through omarchy-notification-send --exec rather than a libnotify
action, because the shell runs clicks from its own omarchy-exec hint and never
emits ActionInvoked. It keeps the default "omarchy-action" app name too, the
only one shouldBypassDnd() lets through -- a crash being the last notification
worth swallowing. It stays quiet until an agent is configured, since a
diagnosis is all it offers.
The method lives in a diagnose-crash skill rather than the prompt, so it is
edited in one place and works with whichever agent is default. It covers
investigating the core, and reporting a confirmed Omarchy bug upstream: scoped
to bugs Omarchy controls, searched for duplicates first, only with the user's
agreement, and signed with the model and harness that produced it.
A migration reaches existing installs, whose skill symlinks and unit enablement
would otherwise sit behind one-time setup paths.
* Let the diagnosis clean up the core it extracted
"Do not modify or delete anything" contradicted the symbolization step right
above it, which writes a core to a temp file and deletes it on exit. Read
literally, the core survives -- and the same section warns it holds passwords
and tokens. The prohibition is about the system, not about your own scratch.
* Do not spend a crash toast on a dead notification server
The shell owns org.freedesktop.Notifications, so its own crash takes the
notification server down with it -- and a shell crash is exactly what you want
told about. The toast was sent once into that gap and the dedupe window was
recorded regardless, so the rest of the crash loop went quiet for a minute and
`journalctl -n 0` never replays what was missed.
It now waits for the restarted shell to reclaim the bus name, as
omarchy-migrate-notify already does, and only a delivered toast starts the
dedupe window.
* Reshape the agent launcher into omarchy agent
omarchy-launch-agent becomes omarchy-agent, with prompts on omarchy-agent-prompt
rather than the bare route: `omarchy agent` is both a command and a group, so a
positional prompt there would shadow any subcommand under it. The launcher takes
flags only and points at `omarchy agent prompt` when handed one.
Every agent window now launches under a fixed org.omarchy.agent app-id instead of
omarchy-launch-tui's default of org.omarchy.<binary>, so one rule floats them all
whichever agent is default.
Omarchy also stops picking an agent for you. omarchy-default-agent prints nothing
until one is chosen, leaving every entry under Setup > Defaults > Agent unchecked,
and a first-run invitation offers to take you there.
* Wordsmith
* Cover the agent routes and the invitation
The route split is the point of the change, so exercise `omarchy agent`,
`omarchy agent prompt`, and a rejected positional prompt through the router
rather than only the binaries behind them.
The invitation gets the same treatment as the Voxtype and fingerprint ones: it
notifies once, opens the agent defaults menu, and leaves both the notification
and the marker alone for anyone who already chose an agent.
* Offer the agent choice from the keybinding
Super + Shift + Ctrl + A now runs `omarchy-agent --pick`, which opens Setup >
Defaults > Agent when nothing is chosen yet. A keypress that writes to stderr
and opens nothing just looks broken.
* Reach existing installs with the agent invitation
first-run installs the invitation hook, and existing accounts marked it complete
long ago, so they would never see it -- while being the accounts most likely to
need it, since the old getter returned opencode implicitly and most have no
agent recorded at all. Post-update hooks run later in the same update, so the
invitation arrives without waiting for another one.
* Say what the Defaults submenus set
Setup > Defaults lists Agent, Browser, Terminal, Editor, but the header inside
each repeated the same bare word, which reads as a category rather than a
setting -- and says nothing at all when the menu is summoned straight into it.
The list keeps its short labels; the headers now name the setting.
Standing in for an abandoned compositor means binding a Unix socket, and the
sandboxes this guard exists for are the ones that deny it: the fixture raised
PermissionError and took the whole file down with set -e, adding a failure in
the environment the guard was written to keep clean. Run the cases that need no
socket first and skip the rest when one cannot be bound.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Read the keyboard being typed on rather than the one holding main
The main flag names no keyboard for long. fcitx5 takes it with the
virtual keyboard it binds to inject, and those are filtered out, so on a
seat running an input method the pick lands on nothing at all: no label,
and the widget hides itself off the bar. #6727 keeps polling in that
state rather than settling it, and the poll has nothing new to read.
Once fcitx5 unbinds, the flag lands on whichever device libinput listed
last, as easily a lid switch as a keyboard, and a device that never
receives the toggle reports the layout it started on forever, which is
the reading #6574 opened.
Every device carries the seat's layout list, but only the keyboard being
typed on advances through it, so read the furthest-advanced one.
activelayout names the keyboard it moved ahead of the layout, so take
that name and let it settle the pick, and the click that switches it.
* Leave the buttons out of the seat the widget reads
Reading the keyboard being typed on left keyboardName standing for two
things at once: the device a click switches, and the device activelayout
last named. Only the second was still being set, so the first went empty
until a switch happened -- which left the click doing nothing on a seat
whose only switch is the click, and left the poll running forever on the
one-keyboard install it was written to leave alone. Give each its own
property, and set the switch target from the reading that confirmed the
keyboard is there.
Layout progress only points at the keyboard being typed on while the
other devices stay where they started, and the ACPI power button, lid
switch and sleep key never do move on their own -- but they answer to
switchxkblayout and can hold the main flag, so anything that reads or
switches whatever the seat hands back can end up describing a button, and
unplugging the keyboard beside one leaves it standing in for the seat.
Drop them where the virtual keyboards are already dropped.
A reading that reaches hyprctl and finds no keyboard now clears the label
rather than leaving a device that is gone described on the bar, told
apart from the empty output a killed query leaves by the device list
itself, and the watchdog asks again rather than waiting for a poll that a
settled seat has already stopped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Skip shell tests when the compositor can't be reached, not just when WAYLAND_DISPLAY is unset
A set variable only proves the environment was inherited. Sandboxes pass it
through while blocking $XDG_RUNTIME_DIR, so Quickshell cleared the guard and
aborted inside QGuiApplication, leaving two core dumps per launch instead of a
clean skip. Probe the socket and, when there's a signature to ask with,
Hyprland itself. Disable core dumps on the way through for the compositor that
dies mid-run, which no probe can catch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Retry the compositor query before calling it dead
Hyprland can miss a query while it reconfigures outputs, and one miss was enough
to skip a whole file's runtime coverage. Retry the way omarchy-launch-shell
does. Only a leftover socket reaches the query at all, so the ordinary skip
still returns immediately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Quickshell watches the QML it loaded and reloads on change, so pacman
replacing /usr/share/omarchy/shell mid-transaction makes the running
shell reload against a half-written tree. That reload fails, and a
failure that reaches the config load is not harmless: it raises the
reload popup, which is a second engine generation.
EngineGeneration::currentGeneration() returns null unless exactly one
exists, so the IPC kill that omarchy-update sends moments later takes
the QCoreApplication::exit(0) branch instead of the generation's own
quit, and Quickshell tears the QML graph down after deleting the
QGuiApplication. The first GUI resource touched on the way out aborts:
FATAL: QPixmap: Must construct a QGuiApplication before a QPixmap
The user gets the crash dialog after an update and a coredump per
occurrence. Reported in #6748 with 3 crashes across 10 updates, always
following a failed reload.
Fixing this in omarchy-update — stopping the shell around the pacman
step — would cover one caller and cost the polkit agent and the
notification server for the length of the transaction, which the
migrations that run next still notify through. It would also have to
carry omarchy-restart-shell's refusal to restart a locked session, or
reintroduce the hazard that refusal exists for.
And omarchy-update is not the only thing that rewrites the tree. The
pacman guard turns away a bare pacman -Syu, but nothing turns away a
targeted pacman -S omarchy, a pacman -U of a locally built package, the
documented OMARCHY_ALLOW_DIRECT_PACMAN bypass, omarchy-dev-pkg-test, or
a checkout in a dev-linked tree.
Turn the watcher off instead. Omarchy has never reloaded through it:
omarchy-restart-shell is what picks up QML changes, and config and
plugin changes go through the shell's own IPC. Third-party plugin hot
reload is PluginRegistry's own inotifywait and FileView watches its own
files, neither of which this touches — QuickshellSettings::watchFiles()
gates the config scanner and nothing else. The popup goes off with it,
because QML can still ask for a reload directly and leave the same
extra generation behind.
Environment reaches Quickshell only at launch, so the update that
delivers this still runs under a watching shell. It takes effect from
the next one.
Verified against an isolated instance: breaking a config in place and
then sending the IPC kill reproduces the FATAL, and it stops with either
variable set. QS_DISABLE_FILE_WATCHER also keeps the failed reload from
happening at all.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drops the ChatGPT web app from the default set at the same time, so installing
the openai-codex-desktop package can't leave two identical-looking ChatGPT
entries in the launcher. Super + Shift + A still opens the web version, which
is the only place it was really used.
The bundled ChatGPT icon stays: the package's own chatgpt.desktop asks for
Icon=chatgpt and ships no hicolor icon of its own.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The vendor-ID guess rejects any device with a driver bound, on the
reasoning that libfprint drives readers from userspace so a real one sits
there unbound. But libusb claims interfaces through a synthetic usbfs
driver, so the reader binds one for as long as fprintd holds the claim —
which is exactly while it is being enrolled or verified against.
Readers that name themselves take the product-string branch and never
reach this, so the exposure is the ones that don't: Goodix 27c6:6594
reports "Goodix USB2.0 MISC", matches on vendor ID alone, and drops out
of detection mid-authentication. The menu entry disappears and the
first-run invitation stops firing while the reader is in use.
Ignore a driver link that resolves to usbfs, and keep rejecting the real
ones — usbio-bridge, usbhid, uvcvideo — including on a device that has
both.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Restore FPC fingerprint detection
* Anchor the FPC product match to a prefix
*fpc* is an unanchored three-letter token on the one branch that is
trusted outright, with none of the kernel-driver checking the vendor
guess gets. Every FPC reader on record leads with it — "FPC Sensor
Controller", "FPC Sensor Controller L:0002 FW:25.26.23.14", and this
branch's "FPC L:0000 FW:1425046" — so requiring the prefix costs no
coverage while keeping three letters from matching mid-string, where
FPC abbreviates unrelated things like flexible printed circuit.
Also restore the note about why Elan is kept out of the vendor list, so
the two signatures read as the same deliberate exception.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Write no product descriptor for a two-field device spec
${remainder#*:} returns the string unchanged when there is no second
colon, so a spec meant to describe a device with no product descriptor
wrote the product id out as its product string instead. Every call site
happened to pass a trailing colon, so the suite was right by accident.
Guard the split and drop the trailing colons, so the two vendor-match
cases exercise the path they were written for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Bind a named driver in the kernel-driver fixture
Touching a bare `driver` file asserts that any driver at all disqualifies
a vendor guess, which is more than the detector should promise: libusb
claims an interface through a synthetic `usbfs` driver, so a reader in
active use looks bound by that rule.
Link the interface at a named driver directory instead, the way sysfs
does. The case still covers what it was written for — a Synaptics bridge
or a camera on a fingerprint vendor ID — without fixing the shape of the
answer for drivers it was never about.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The widget polled hyprctl every 10 seconds per monitor, including on the single-layout install where it never shows. Keep the poll only where its answer can change - a seat with more than one keyboard, where Hyprland moves the main flag with no event to announce it - and stop it entirely once a one-keyboard seat has been read.
Also coalesce a refresh that arrives mid-query instead of dropping it, time out a query that never returns rather than letting it hold the guard shut for good, and re-read the layout on configreloaded.
Co-Authored-By: markbus-ai <markbus-ai@users.noreply.github.com>
* Replay the history a dismissal or a clear was still being written into
The popup files a replay reads are written by a serialized queue of shell
jobs, and the read ran as its own process alongside it. A dismissal issued a
moment earlier could still be queued when the directory was read, leaving the
notification out of the replay it was the newest entry of, and a clear issued
a moment earlier could still be queued too, replaying entries it was about to
remove.
The read now waits for the queue to go idle, so the replay shows the history
as of the moment it was asked for rather than whichever jobs happened to have
landed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Catch up on an update that arrived before its popup had a row
Watching a notification for in-place updates starts the moment it is handed
over, but the row those updates write to is inserted a tick later, deferred to
keep a mid-incubation Repeater from being mutated underneath. A client fast
enough to update inside that window found no row to write to, and a property
that has already changed does not change again — so the toast and its file sat
on the superseded content until something else moved.
The row is now refreshed from the live notification once it exists. That reads
the same object the signals would have, so an update that beat the insert is
picked up and one that did not costs nothing: a refresh whose content matches
the row it would write is dropped, which also collapses the several signals a
single multi-property update emits into one rewrite.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Hold queued file work behind the replay's read, not just ahead of it
The read waited for everything queued before it, but nothing stopped the queue
from running on while it worked. A clear or an archive issued during the read
could delete or move files out from under awk mid-glob, so a replay could still
show a partial history — some of what a clear was in the middle of emptying.
The read is a barrier in both directions now: the queue holds until it exits,
and it releases on exit rather than on output, so a read that comes back empty
or fails cannot park the queue behind it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Queue the replay's read instead of waiting for the queue to empty
Waiting for the queue to go idle before starting the read still let work
overtake it. A clear or an archive enqueued after the replay was asked for,
while the current job was running, was dequeued the moment that job exited —
the read only starts once nothing is left — so the replay showed the state
after those jobs, which is the race this was meant to close. Unbroken file
traffic could postpone the read indefinitely for the same reason.
The read is now an entry in that queue rather than a process running beside
it. It takes its place in line behind the work queued before the request and
ahead of everything queued after, so no later job can overtake it and no
amount of traffic can push it back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The pacman cache grows without bound across updates, and nothing in the
update flow ever reclaimed it. On a machine that has been updating for a
while it reaches several gigabytes of superseded versions that nothing
will ever install again.
Prune it with paccache -rk2 as the first step of an update. Both halves
of that placement are load-bearing.
Keeping two versions rather than one preserves the rollback path. The
cache is Arch's only offline downgrade: when an update breaks a single
package, reinstalling its predecessor from here is the surgical fix,
where a snapshot rollback would revert every other package too. Pruning
before the packages update means the installed version is still the
newest cached, so it survives along with a spare. Retention is by
version order and never consults what is installed, so that holds while
the installed version is among the two newest cached; a deliberate
downgrade or repeated failed transactions can stack newer archives on
top of it.
Running before the snapshot is what actually frees the space. The cache
sits on the snapshotted root subvolume, so a prune taken afterwards
leaves the fresh snapshot holding those extents and reclaims nothing
until it ages out of the number cleanup.
A failed prune warns and continues. Cache housekeeping should not trip
the update's ERR trap and tell the user their update went wrong.
This runs after omarchy-update-requires-free-space, so it reclaims space
during healthy updates but does not rescue a machine already under the
10 GiB gate.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
A client that updates a notification through replaces_id does not produce a
second onNotification: Quickshell writes the new content onto the Notification
object the shell is already holding. The card draws a snapshot copied out of
that object — deliberately, since a live QObject in a ListModel role becomes a
dangling pointer the moment the server destroys it — so the toast kept showing
the superseded text, and archived it to history when it left the screen. A
Slack thread that updates in place read as stuck.
Every property the card draws is now watched on the notification we hold, and
a change rewrites both the model row and the file the popup was persisted
under. The file name is that popup's identity, so the rewrite lands in place:
a shell restart restores the version last shown, and so does the copy that
reaches history.
The countdown starts over when the content changes. New text arriving a second
before the toast was due to expire deserves a full look, not the remainder of
the clock the text it replaced had nearly run through.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
History was a pair of in-memory lists mirrored into notifications.json, split
into "pending" and "past" by a seen/unseen distinction no surface exposed,
capped at 100, deduped by an id that repeats across server generations, and
pruned by a 15-minute TTL. Replaying it showed five rows drawn from whichever
list happened to hold them.
Every toast already writes a file under ~/.local/state/omarchy/notifications
so it can survive a shell restart. That file is now the history record: when
the popup leaves the screen it moves into notifications/history instead of
being deleted, the newest ten are kept, and showHistory replays exactly what
is in there, including the toasts still on screen when it is asked for. A
notification DND silenced is written straight into the same directory, since
a toast that never showed is the one worth looking back at.
That leaves the models, notifications.json history payload, past pruning, and
the /tmp image cache that existed to keep century-old history thumbnails alive
with nothing to do, so they go.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every supported agent spells it differently, so map each one to its own
bypass flag instead of leaving the launcher at each agent's default.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1784917531 gated its UKI rebuild on initramfs_async=0 being present in
the Limine config, but omarchy-settings ships omarchy-defaults.conf with
that parameter already in it. Any machine that installed the package and
ran the migration in the same update matched the config the package had
just written, skipped the rebuild, and kept booting an image baked
before the config existed — without initramfs_async=0, so encrypted
boots still fell back to an unthemed text LUKS prompt. Compare the
booted command line against the configured one and rebuild when they
disagree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The stub polled for the lock with its own flock, competing with the
holder it had just started. The holder took the lock non-blockingly and
never retried, so a lost race killed it and left the lock free. The
notifier then saw no update in progress and sent the toast the test
asserts it withholds. Wait on the holder's own signal instead.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The sleep inhibitor deliberately outlives the start that spawns it, but
script tears its pty down as soon as the command returns, and the SIGHUP
that follows could kill the inhibitor before it managed to exec. The
sudo stub then never logged and the test failed about half the time.
Hold the session open from inside until the inhibitor has started.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
omarchy-monitor-state stopped shelling out to
omarchy-hyprland-monitor-focused when it started deriving the focused
name from its own hyprctl snapshot, so the stub the test installed was
never called and the assertion could never pass. Expect the focused
monitor from the fixture instead, and drop the dead stub.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Accept either name for the lock authentication command
omarchy-setup-lock was renamed to omarchy-apply-lock in 536fcd5c, but
the upgrade calls into whatever the channel just installed, and every
released package still ships the old name. The rename only moves in
lockstep for the ISO, which installs the runtime from the mirror it
ships with; the upgrade has no such guarantee, so it aborted every run
with "omarchy-apply-lock is unavailable" right before the point of no
return.
Prefer the new name and fall back to the old one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Leave the Omarchy 3 session alone until the reboot
The upgrade tried to start Quickshell in the live session, and only
stopped waybar/walker/elephant if that worked. Every branch of it could
fail, so it needed a warning for each, and those warnings were the first
thing users read at the end of an upgrade that had otherwise succeeded.
The reboot is the cutover. Swapping the UI out underneath a running
session buys nothing, so drop the attempt and both functions with it.
The Omarchy 3 bar, launcher, and notifications keep working until the
reboot, which is what happened anyway whenever the start failed.
Also warn up front when the live Hyprland session cannot be reached, and
drop the package_mode label that was set on both branches of the
dev-package check and never read.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Shim the legacy Hyprland defaults from the backup, not from GitHub
populate_legacy_hypr_defaults tried a sparse git clone of master, then a
curl of the master tarball, and only if both failed the backup taken a
few lines earlier. That backup is the checkout the running session is
sourcing right now: it is the correct content, it is already on disk,
and it needs no network in the middle of an upgrade. master is a guess
that is wrong for any machine not on master.
Try the backup first and drop the git clone, which fetched the same
thing as the tarball by a longer route. The network path stays for the
case that has no backup, where the legacy root was already a symlink.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Clear the Hyprland error bar the upgrade leaves behind
The two settings were applied through both the keyword and the Lua API,
in four calls on the way out and three on the way in, split differently
each time. Fold that into one helper that takes the value.
The error bar needed more than suppression. An explicit hyprctl reload
re-reads the config from disk, which resets both keywords before it
reports what it found, so suppression cannot survive one; anything that
reloaded during the swap left the bar on screen. Hyprland then keeps it
up until a later clean reload, which this script deliberately never
performs, so it was still there when the upgrade finished. Clear the
overlay on the way out, once the shims have made the legacy config
resolve again.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Run the packaged firewall config instead of a copy of it
configure_snapper_policy and configure_lock_authentication already call
into the installed tree; apply_firewall_defaults reimplemented
install/config/firewall.sh inline instead, and had already drifted from
it. The packaged script also installs the ufw-docker rules, so upgraded
machines came up without the Docker firewall protections a fresh install
gets.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Stop the Voxtype toggle migration reloading a mid-swap session
This is the reload behind the "source= globbing error" bar users see
during a Quattro upgrade. The upgrade runs the packaged migrations
against a still-running Omarchy 3 session, and this one ends with
hyprctl reload. That re-parses a legacy config whose theme source= has
nothing to resolve to yet, so Hyprland paints an error bar and keeps it
up until a later clean reload the upgrade deliberately never performs.
It was still on screen when the upgrade finished.
The upgrade already exports OMARCHY_UPGRADE_TO_QUATTRO_LIVE for exactly
this, and 1782002156 honors it. Do the same here. Nothing in that
session reads the toggle being removed; the reboot applies it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Assert the retired session entry points stay gone
The ordering check would still pass if either came back, while the
comment above it claims they cannot. Name them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Upgrade onto the channel the machine is already on
The upgrade always defaulted to stable, so an rc machine taking it
landed on production packages from the stable repo. rc callers worked
around that by passing --channel rc, which then forced every caller
onto rc, stable machines included.
Read the channel off the mirrorlist the way omarchy-version-channel
does and follow it: stable machines get omarchy and omarchy-settings
from stable, and rc or edge machines get omarchy-dev and
omarchy-settings-dev from edge, which is what --dev already selects.
An explicit --channel or --dev still wins, and an unrecognized
mirrorlist still falls back to stable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Keep rc machines on the rc mirror when taking dev packages
--dev forced the edge channel, so an rc machine following its own
channel was moved onto the edge Arch mirror as well. The constraint is
narrower than that: the dev packages are only published to the edge
package repo, which the rc and edge channels both already point at.
Only stable is incompatible.
Reject --dev only for stable, and default to edge just when no channel
was chosen. rc machines now upgrade against rc-mirror with the dev
packages out of the edge repo, which is where Quattro lives until it
ships.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Repair theme symlinks the state-move migration left dangling
1781043107.sh re-linked legacy theme symlinks whose targets were stored
with a literal "~/.config/omarchy/current/..." string. The replacement
used the same literal tilde, which the filesystem never expands inside a
symlink target, so btop, Helix, and VS Code/Cursor lost their theme and
the migration reported success anyway.
Fix the source migration to relink through the already-defined
$current_state_dir variable, and add a follow-up migration that repairs
the links the applied version left dangling — matching the existing
1785002349.sh pattern, so it is idempotent and leaves custom links alone.
Co-Authored-By: Claude <noreply@anthropic.com>
* Only repair theme symlinks that could never have worked
The repair matched any target containing omarchy/current, so a working link
into a user's own dotfiles was rewritten to the state directory and their
setup was lost. Claim a link only when its target starts with a literal "~/",
which the filesystem never expands, and names this exact theme file: that is
what 1781043107.sh wrote, and no working link can look like it. A dangling
target is not enough on its own, since a dotfiles repo may just be unmounted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Clean up every scratch directory the CLI suite creates
Five of the eight mktemp directories were never registered with the exit
trap, so each run left them behind in /tmp. Route them all through a helper
that records them for cleanup.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Drop the theme symlink migration tests
A migration runs once on each machine and is then inert, but a test for it
sits in the suite forever. Now that the repair behaves correctly, keep the
migration and let it go untested rather than grow the suite permanently.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Every other pointer at the community lives in an error path or a doc, so
there was no way to reach it from the menu. Prefer the Discord app when
it is installed, and fall back to the invite in a browser when it is not.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The row list was capped at 60% of screen height so a card could never read
as a page. On a laptop-height display that folds the starting menu one row
early, hiding About behind the peek for no gain.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Herdr ships its own annotated keybindings menu, same as Tmux, but nothing
in the menu pointed at it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
* 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>
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>
* 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>
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).
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>
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>
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>
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>
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>
* 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>
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>
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>
* 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>
* 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>
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>
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>
* 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>
* 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>
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>
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>
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>
* 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>
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>
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>
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>
* 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>
* 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>
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>
* 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>
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.
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.
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>
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>
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.
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>
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>
/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>
* 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>
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>
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>
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>
* 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>
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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
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>
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>
* 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>
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>
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>
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>
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>
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>
* 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>
* 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>
* 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>
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>
* 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>
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>
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>
* 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>
* 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>
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>
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>
* 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>
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>
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>
* 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>
* 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>
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>
* 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>
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
* 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>
* 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>
* 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>
* 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>
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>
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>
* 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>
* 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>
* 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>
* 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.
* 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>
* 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>
Leave agents/ ready for other agent artifacts like definitions and
prompts alongside the skills.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
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>
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>
* 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>
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>
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>
* 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>
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>
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>
* 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>
* 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>
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>
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>
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>
* 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>
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
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>
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>
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>
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>
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>
* 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>
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.
* 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>
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>
* 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>
* 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>
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>
* 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>
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>
* 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>
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>
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.
- 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.
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>
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.
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>
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>
* 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
* 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>
* 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>
* 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>
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>
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>
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>
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>
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>
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 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>
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>
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>
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>
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>
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>
* 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>
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>
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>
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>
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.
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>
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
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
* 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>
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>
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>
* 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>
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>
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-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>
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>
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>
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>
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>
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>
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
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
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>
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
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
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
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
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
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.
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>
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>
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>
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>
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
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>
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>
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
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>
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>
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>
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
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.
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.
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>
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
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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
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>
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>
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>
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>
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>
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>
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>
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>
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.
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.
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.
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>
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>
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>
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>
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>
* 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>
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.
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
* 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>
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.
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>
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>
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>
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>
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>
* 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>
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>
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.
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>
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>
* 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>
* 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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`
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.
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.
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>
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>
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>
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>
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).
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.
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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
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.
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.
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>
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.
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.
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).
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.
- 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.
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.
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.
Remove legacy online installer entrypoints, collapse migrations for 4.0, and move setup responsibilities into target-side system, hardware, and user commands.
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.
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.
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.
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.
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).
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.
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.
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.
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).
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.
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.
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.
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.
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.
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
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.
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.
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).
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).
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'.
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.
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.
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.
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.
- 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.
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.
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).
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.
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.
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.
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.
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.
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.
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-*.
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.
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.
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.
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.
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.
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.
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).
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).
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.
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.
- 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).
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.
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.
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.
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.
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.
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.
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
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.
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.
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>
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').
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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".
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.
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.
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.
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.
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.
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.
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>
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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).
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.
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
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.
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.
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.
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.
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
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.
`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.
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).
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.
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.
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.
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.
- 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.
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.
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.
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.
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.
- 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.
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.
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.
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.
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>
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.
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.
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.).
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.
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.
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.
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.
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).
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.
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.
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.
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.
- 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.
- 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.
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]
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
- 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.
- 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.
- 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.
- 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.
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.
* 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
* 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>
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>
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
* 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.
* 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>
* 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>
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.
* 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>
* 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
* 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>
`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.)
* 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
* 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>
* 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
* 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
* 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>
* 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>
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.
* 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>
* 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
* 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>
* 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>
* 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>
* 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.
* 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>
* 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>
* 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>
* 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
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
* 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>
* 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>
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>
* 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
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
* 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>
* 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>
* add generation of colors.toml via alacritty.toml
* Shorter command name
* Fix perms
---------
Co-authored-by: David Heinemeier Hansson <david@hey.com>
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.
* 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>
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>
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>
* 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
* 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>
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.
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>
* 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>
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
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
* 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.
* 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>
* 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>
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.
* 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.
If you believe you’ve found a security vulnerability in Omarchy, please tell the [Omarchy Security Team](https://omarchy.org/teams/#security) privately so we have an opportunity to investigate and fix it before it is made public.
Please don’t report potential vulnerabilities publicly in GitHub Issues, Discord, or social media before they’ve been resolved.
## What is a vulnerability?
We consider a bug a security vulnerability when it can be exploited to cross a meaningful security boundary: an untrusted or lower-privileged party gains access, permissions, or control they didn’t already have.
Code that could be more robust but does not cross a security boundary is an improvement rather than a security vulnerability. We may still merge a proposed fix and credit the reporter in our release notes.
Eligibility for our [security credits](https://omarchy.org/security/credits/) page depends on whether a report identifies a confirmed security vulnerability, not on its severity.
## What to include
Give us enough information to understand and reproduce the issue:
- The affected component and Omarchy version.
- An explanation of what an attacker can do before and after exploitation.
- Steps to reproduce the issue and any proof of concept.
- Your preferred contact details for follow-up.
## Responsible disclosure
Please act in good faith while investigating and reporting vulnerabilities:
- Only test systems and accounts you own or have explicit permission to test.
- Avoid privacy violations, disruption, data destruction, and service degradation.
- Don’t exploit a vulnerability beyond what is needed to demonstrate it.
- Give us a reasonable opportunity to investigate and address the issue before publishing details.
We’ll review your report and keep you informed as we’re able while we work toward a resolution.
## Credits
Researchers who privately report a confirmed security vulnerability and give us the chance to ship a fix are thanked on the [security credits](https://omarchy.org/security/credits/) page. Accepted improvements that don’t cross a security boundary may still be credited in our release notes.
Credits link to each reporter’s X profile and show their avatar. For duplicate reports, only the first reporter is eligible for credit.
## Regular bugs and support
For anything that isn’t a security vulnerability, please use the [Omarchy issue tracker](https://github.com/omacom/omarchy/issues).
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
# 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
- Two spaces for indentation, no tabs
- Use bash 5 conditionals: use `[[ ]]` for string/file tests and `(( ))` for numeric tests
- 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" ]]`)
- In `[[ ]]`, don't quote variables, but do quote string literals when comparing values (e.g., `[[ $branch == "dev" ]]`)
- 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`)
- 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`)
- Shebangs must use `#!/bin/bash` consistently (never `#!/usr/bin/env bash`). A security-sensitive entrypoint may use the exact `#!/bin/bash -p` form only when it must suppress `BASH_ENV` and exported-function startup injection before its first command; that exception must be explained at the boundary and covered by a regression that rejects an ordinary Bash launch with a decoy `-p` argument.
- Scripts under `install/` and `migrations/` may be sourced and intentionally omit shebangs
# Command Naming
# 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
-`cmd-` - check if commands exist, misc utility commands
-`capture-` - screenshots, screen recordings, and other capture tools
-`pkg-` - package management helpers
-`pkg-` - package management helpers
-`hw-` - hardware detection (return exit codes for use in conditionals)
-`hw-` - hardware detection (return exit codes for use in conditionals)
-`refresh-` - copy default config to user's `~/.config/`
-`refresh-` - copy default config to user's `~/.config/`
@@ -23,45 +54,121 @@ All commands start with `omarchy-`. Prefixes indicate purpose:
-`theme-` - theme management
-`theme-` - theme management
-`update-` - update components
-`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.
# Platforms
Omarchy runs on x86 and aarch64 (ARM). A platform name is the CPU architecture, then a hardware family only when that family needs handling of its own:
-`x86` - every x86_64 machine
-`aarch64` - every ARM machine without a family of its own (Snapdragon laptops, Raspberry Pis and ARM VMs today)
-`aarch64-apple` - Apple Silicon Macs
A new family is named `aarch64-<family>` (e.g. `aarch64-qualcomm`, `aarch64-n1x`) and is added only once it needs code of its own, so `aarch64*` always means ARM. `omarchy-hw-platform` prints the most specific name. Use these exact names wherever a platform is named: package lists, pacman template directories, image manifests, dispatch registration, tests and docs. Never introduce `generic`, `arm64`, `apple-silicon` or other spellings; the old `apple-silicon`, `generic-aarch64` and `generic` are read only for compatibility with older image builders.
Ask a helper; don't read `uname -m` or the device tree in feature code:
-`omarchy-hw-x86` / `omarchy-hw-aarch64` - the CPU architecture. Use them for binary and ABI availability: a package with no aarch64 build, an x86 guest VM, multilib.
-`omarchy-hw-aarch64-apple` (and later `omarchy-hw-aarch64-<family>`) - built on `omarchy-hw-platform`, image-build aware. Use them for hardware behaviour. `omarchy-hw-apple-silicon` is a hidden alias kept for the omarchy-mac packages; don't use it in new code.
Where platform code lives:
- Packages: `install/omarchy-base.packages` for everyone, `install/omarchy-aarch64.packages` for every ARM machine, `install/omarchy-<platform>.packages` for one family, and base packages with no aarch64 build in `install/omarchy-x86_64-only.packages`. `omarchy-pkg-defaults` composes them.
- Pacman templates: `default/pacman/` for x86, `default/pacman/<platform>/` for each ARM platform.
- Hardware setup: `install/hardware/<vendor-or-family>/`, gated by a predicate. `install/hardware/apple/` holds Intel Mac quirks; Apple Silicon's own setup ships in the omarchy-mac packages.
- Boot chains Omarchy can't drive generically: a platform package behind `omarchy-lifecycle-dispatch` ([`docs/lifecycle-dispatch.md`](docs/lifecycle-dispatch.md)). Platforms that boot Limine with UKIs like x86 keep the generic path.
The experience should be the same on every platform. When a package or feature is missing or broken on one, the default is to get it fixed upstream (an aarch64 build, a bug report, a patch to the maintainer) and leave the gap visible meanwhile, not to paper over it. When asked for a platform workaround, say so and propose the upstream fix first. Hiding a menu entry or skipping a package is fine only for things that can't exist there (an x86 guest VM, an x86-only proprietary binary). Alternative packages and platform-only replacements are a last resort, used only when upstream can't or won't fix it, with the reason written next to the code.
# 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
# Helper Commands
Use these instead of raw shell commands:
Use these instead of raw shell commands:
-`omarchy-cmd-missing` / `omarchy-cmd-present` - check for 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-add` - install packages from the configured pacman repos, Omarchy's included; use `omarchy-pkg-aur-add` for AUR packages
-`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)
-`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.
# Background Work
- Long-running code (the shell, daemons, watchers) reacts to events instead of polling with a timer. Polling is the last resort, for state with no change signal; [`agents/skills/shell-dev.md`](agents/skills/shell-dev.md) lists the event sources and the push-refresh pattern.
# 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.
# Language
-`omarchy-language en|zh` (Setup > Language) owns `LANG` in `/etc/locale.conf`; the installer sets it from its first screen. Nothing in `omarchy update` or migrations may change `/etc/locale.conf`.
- Chinese menu labels live in `default/omarchy/omarchy-menu.zh-cn.jsonc`, merged when `LANG` starts with `zh_CN`; give a new menu row with translatable words a label there.
- Shell text goes through `I18n.tr("English %1", arg)` from `qs.Commons`, with its Chinese in `shell/Commons/I18nZh.js` keyed by the English. Notifications and OSD messages are worded by their sender with `omarchy-text "English %1" "$arg"` (Python: `text()`, QML: `I18n.tr`), which reads the same table; the shell shows them as given, so user text is never translated. `test/shell.d/shell-i18n-test.sh` fails on a missing entry.
# Region
-`/etc/omarchy/region` holds the region the installer derived from the timezone (`cn`); no file means Global. The profile lives in `default/regions/cn/`, its contract in `docs/regions.md`.
-`install/config/region.sh` links the `cn` developer mirrors into `/etc` and `/.cargo` and refuses to replace a regular file it did not create. It serves both the install (the marker) and `omarchy region cn|global` (`OMARCHY_REGION`), which also records or removes the marker; Global removes only the links it made.
-`omarchy-region` prints the region (`Global` or `China`) and switches it. Nothing in `omarchy update` or migrations may change the region.
- China-only menu rows carry `"when":"[[ \"$(omarchy-region)\" == \"China\" ]]"` on every row, not only the submenu, because search checks each row's own guard. User-facing docs: `manual/52-china.md`.
# Config Structure
# Config Structure
-`config/` - default configs copied to `~/.config/`
-`config/` - default configs copied to `~/.config/`
-`default/themed/*.tpl` - templates with `{{ variable }}` placeholders for theme colors
-`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/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
Then confirm it in the running menu per [`visual-verification.md`](visual-verification.md). Avoid leaving two fonts with the `omarchy` family registered: Qt can use an old copy in `~/.local/share/fonts` even when `fc-match omarchy` reports the packaged font. For a preview, either replace the packaged file in the disposable VM or temporarily exclude it with a `<rejectfont>` rule in `~/.config/fontconfig/conf.d/` before loading the candidate.
Refresh the font cache and restart the shell afterwards — Qt reads the font database at startup, so `omarchy menu refresh` alone will not pick up a changed font. Remove temporary fonts and rules after verification.
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.
- 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`.
- in an image build every `run_logged` leaf in `install/hardware/all.sh` is queued and runs on the machine's first boot instead (`bin/omarchy-provision-hardware`, see `docs/file-layout.md`), possibly offline, using packages already in the image, and before any user exists; keep those leaves idempotent and put nothing but `run_logged` lines in `all.sh`.
- gate platform-specific setup on a predicate built on `omarchy-hw-platform` (`omarchy-hw-aarch64-apple`), not on `uname -m`; architecture checks (`omarchy-hw-x86`, `omarchy-hw-aarch64`) are only for ABI, binary availability or a repository's `$arch`. Platform names and where platform code lives are in the Platforms section of `AGENTS.md`.
- 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.
- The base install supplies matching kernel headers before hardware setup. DKMS installers should install their driver packages and assume the headers exist.
- 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.
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"
Keep a dedicated test while the migration is still being written or bugfixed, if it calls an Omarchy helper whose interface can still change, or if it is a security-sensitive privileged repair (FIDO2, leftover installer artifacts, udev, sshd). Once a one-shot rewrite has shipped in a tagged release and is frozen, drop the test even when that rewrite used sudo, pacman, or limine-mkinitcpio. Keep the migration itself for late-updaters. Tests of `omarchy-migrate`, the login notifier, and `omarchy-upgrade-to-quattro` stay.
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.
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. First-party plugins receive the host objects. Third-party plugins receive capability-scoped facades: ordinary plugins may look up and control only their own service and lifecycle, built-in clones retain narrow source-specific configuration and UI compatibility, menu plugins receive an application-library facade, and plugins can read detached scalar bar state; full-bar plugins additionally receive detached bar configuration and widget-catalog snapshots, narrow proxies for the non-authentication services used by built-in bar widgets, and lifecycle control over configured non-authentication UI plugins. Authentication capabilities must be stamped from trusted first-party manifests, and third-party registry views and bar configuration must be detached snapshots rather than shared objects. These facades reduce accidental authority but are not a same-process QML sandbox: a visual bar widget can walk its parent hierarchy to ordinary host objects. Authentication services must therefore remain outside both `ShellRoot._services` and the host QObject tree. Do not expose authentication services through new third-party-facing properties.
- Panel / overlay / menu plugins must expose `open(payloadJson)` and
`close()` lifecycle methods for `shell summon` and `shell hide`.
## Text
Wrap every user-visible string in `I18n.tr()` (`import qs.Commons`) and add its Chinese to `shell/Commons/I18nZh.js`; plain JS models that Node tests load stay English, and their text is wrapped where QML shows it.
## 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
`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.
## Keeping state current without polling
The shell runs all day on battery, and a widget is built once per monitor, so a `Timer` that starts a `Process` costs that process times the number of monitors, every tick, forever. Don't poll for state that only changes through known events. Pick the first of these that fits:
- A native Quickshell service that signals on change: `UPower` and `PowerProfiles` (`Quickshell.Services.UPower`), `Hyprland` and each workspace's `lastIpcObject`, `Pipewire`, `Mpris`, `SystemTray`, `Notifications`.
- An event stream the shell already runs, such as the Hyprland IPC events or the long-running `pactl subscribe` in `Commons/UntypedInput.qml`. Extend it rather than starting a second one.
- A file watch: `FileView` with `watchChanges`, or the directory watch in `plugins/bar/Bar.qml`, when the state lives in a file the user can read.
- A push from the command that changes the state. If only Omarchy commands change it, have them call `omarchy-shell -q <target> refresh` after the change, the way `omarchy-reminder` and `omarchy-sudo-passwordless` do. When the change happens later, without a command running (an expiry, say), schedule the refresh with a transient `systemd-run --user --on-active=…` timer. A root-owned timer cannot reach the user's shell.
Polling is the last resort, kept for state with no change signal at all (a remote API, say). Then gate the timer on the panel being open where you can, pick the longest interval the UI can tolerate, and put the poll in one shared service rather than in a per-monitor widget. Never poll a privileged probe: every `sudo -n` can write to the auth log.
## 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)`.
## Palette references
Import `qs.Commons as Commons` and use `Commons.Color` for the palette singleton, including `Connections.target`. Qt 6.12 introduces a `Color` type in `QtQuick` that shadows unqualified palette references. This applies to first-party components and third-party plugin QML alike. Keep the unqualified `qs.Commons` import when the file also uses `Style`, `Util`, or `Border`.
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 writes to Omasnap's configured screenshot directory. Use `omarchy screenshot` for the interactive capture flow, which copies the capture and shows a timed preview. Use `omarchy screenshot --editor=overlay` to test annotation before output. 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
# omarchy:summary=Configure the bar and its widget layout
# omarchy:group=bar
# omarchy:args=use <id> | reset | defaults | position [top|bottom|left|right] | background [opaque|glassy|none|cycle] | put <id> [placement] | move <id> [placement] | set <id> <key> <value> [--json] [placement] | get <id> <key>
# 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; prints the current one
when given none
background [opaque|glassy|none|cycle]
Bar background: glassy shows what is
behind the bar blurred, cycle steps
none, glassy, opaque; prints the
current one when given none
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
get <id> <key> Print 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-notification-send -g -u critical "$(omarchy-text "Time to recharge!")" "$(omarchy-text "Battery is down to %1%" "$level")" -i battery-caution -t 30000
omarchy-hook battery-low "$level"
Loaded 100 of 2806 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.