* 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>
* 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>
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>
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>
* 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>
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>
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 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.
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
Remove legacy online installer entrypoints, collapse migrations for 4.0, and move setup responsibilities into target-side system, hardware, and user commands.