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