Ship the keyboard layout widget on the bar and make clicking it work (#6659)

* 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>
This commit is contained in:
David Heinemeier Hansson
2026-08-09 19:38:46 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent 6ddc39520d
commit e1d0c4e0a8
6 changed files with 132 additions and 17 deletions
+66 -14
View File
@@ -2,8 +2,8 @@
# omarchy:summary=Configure the bar and its widget layout # omarchy:summary=Configure the bar and its widget layout
# omarchy:group=bar # omarchy:group=bar
# omarchy:args=use <id> | reset | defaults | position <top|bottom|left|right> | transparent <true|false|toggle> | move <id> [placement] | set <id> <key> <value> [--json] [placement] # omarchy:args=use <id> | reset | defaults | position <top|bottom|left|right> | transparent <true|false|toggle> | put <id> [placement] | move <id> [placement] | set <id> <key> <value> [--json] [placement]
# omarchy:examples=omarchy bar use local.neon-bar | omarchy bar move omarchy.clock --section center --index 0 | omarchy bar set omarchy.clock format HH:mm # 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 set -euo pipefail
@@ -18,6 +18,8 @@ Usage: omarchy bar <command> [args...]
defaults Restore the default bar and service widgets defaults Restore the default bar and service widgets
position <top|bottom|left|right> Bar position position <top|bottom|left|right> Bar position
transparent <true|false|toggle> Bar transparency transparent <true|false|toggle> Bar transparency
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 move <id> [placement] Move a widget within or between sections
set <id> <key> <value> [--json] [placement] set <id> <key> <value> [--json] [placement]
Set a per-widget option Set a per-widget option
@@ -33,8 +35,12 @@ Placement:
Enable and disable widgets with 'omarchy plugin enable' and Enable and disable widgets with 'omarchy plugin enable' and
'omarchy plugin disable'. 'omarchy plugin disable'.
'put' looks for --before / --after within the section it is adding to, and
falls back to the end of that section when the named widget is not there.
Examples: Examples:
omarchy bar use local.neon-bar 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.media left
omarchy bar move omarchy.clock --section center --index 0 omarchy bar move omarchy.clock --section center --index 0
omarchy bar set omarchy.clock format HH:mm omarchy bar set omarchy.clock format HH:mm
@@ -152,22 +158,14 @@ cmd_use() {
cmd_defaults() { cmd_defaults() {
(( $# == 0 )) || fail "defaults does not take arguments" (( $# == 0 )) || fail "defaults does not take arguments"
local catalog
local optional_widgets="[]" local optional_widgets="[]"
local service widget local service widget
catalog=$(omarchy-plugin-catalog)
for service in dropbox tailscale; do for service in dropbox tailscale; do
if "omarchy-installed-service-$service"; then if "omarchy-installed-service-$service"; then
widget=$(jq -c --arg id "omarchy.$service" ' widget=$(jq -cn \
map(select(.id == $id))[0] as $plugin --arg id "omarchy.$service" \
| { --arg section "$(bar_widget_default_section "omarchy.$service")" \
id: $id, '{id: $id, section: $section}')
section: (
$plugin.barWidget.defaultSection // "center"
| if IN("left", "center", "right") then . else "center" end
)
}
' <<<"$catalog")
optional_widgets=$(jq -c --argjson widget "$widget" '. + [$widget]' <<<"$optional_widgets") optional_widgets=$(jq -c --argjson widget "$widget" '. + [$widget]' <<<"$optional_widgets")
fi fi
done done
@@ -219,6 +217,57 @@ cmd_transparent() {
fi fi
} }
bar_widget_default_section() {
local catalog
catalog=$(omarchy-plugin-catalog 2>/dev/null) || {
echo "center"
return 0
}
jq -r --arg id "$1" '
map(select(.id == $id))[0].barWidget.defaultSection // "center"
| if IN("left", "center", "right") then . else "center" end
' <<<"$catalog"
}
# Placement lives in the shell, which owns the config it has in memory. Putting
# a widget therefore asks the shell rather than editing the file behind it.
cmd_put() {
local id="${1:-}"
[[ -n $id ]] || fail "put requires a widget id"
shift
local positional_section=""
if (( $# > 0 )) && [[ $1 != --* ]]; then
positional_section="$1"
validate_section "$positional_section"
shift
fi
parse_placement "$@"
[[ -z $positional_section || -z $PLACEMENT_SECTION ]] ||
fail "specify a section positionally or with --section, not both"
[[ -z $positional_section ]] || PLACEMENT_SECTION="$positional_section"
[[ -z $PLACEMENT_FROM_SECTION && -z $PLACEMENT_FROM_INDEX ]] ||
fail "put does not accept --from-section or --from-index"
# Nothing to place into with no shell to place it, and a caller that runs
# unattended should say so and carry on rather than fail. A shell that is
# there and refuses is a real error.
if ! omarchy-shell shell ping >/dev/null 2>&1; then
echo "omarchy-shell is not running; $id was not put on the bar" >&2
return 0
fi
local result
result=$(omarchy-shell shell putBarWidget "$id" "$(placement_json)") ||
fail "could not put $id on the bar"
[[ $result != "unknown" ]] || fail "$id is not a known widget; run 'omarchy plugin list'"
[[ $result == "ok" ]] || fail "$result"
# Says nothing about whether it had to be placed: a widget already on the bar
# is left where it is, and both outcomes are the same answer to the caller.
echo "$id is on the bar"
}
cmd_move() { cmd_move() {
local id="${1:-}" local id="${1:-}"
[[ -n $id ]] || fail "move requires a widget id" [[ -n $id ]] || fail "move requires a widget id"
@@ -302,6 +351,9 @@ case "$command" in
transparent) transparent)
cmd_transparent "$@" cmd_transparent "$@"
;; ;;
put)
cmd_put "$@"
;;
move) move)
cmd_move "$@" cmd_move "$@"
;; ;;
+3
View File
@@ -27,6 +27,9 @@
"formatAlt": "d MMMM 'W'ww yyyy", "formatAlt": "d MMMM 'W'ww yyyy",
"verticalFormat": "HH\n\u2014\nmm" "verticalFormat": "HH\n\u2014\nmm"
}, },
{
"id": "omarchy.keyboard-layout"
},
{ {
"id": "omarchy.weather" "id": "omarchy.weather"
}, },
+9
View File
@@ -0,0 +1,9 @@
echo "Add the keyboard layout widget to the bar"
# The widget is now in the default layout, sitting just right of the clock. It
# hides itself unless the active keyboard has more than one layout configured,
# so adding it to every existing bar is free: a machine on a single kb_layout
# never sees it. A bar that already carries the widget keeps it where the user
# put it.
omarchy-bar put omarchy.keyboard-layout --after omarchy.clock
+10 -3
View File
@@ -13,6 +13,10 @@ BarWidget {
property string layoutLabel: "" property string layoutLabel: ""
property string layoutFull: "" property string layoutFull: ""
property string keyboardName: "" property string keyboardName: ""
// Nothing to read or switch on the single-layout install most people run, so
// the widget ships on the bar and stays out of the way until there are two.
// An older Hyprland that doesn't report the list keeps showing the label.
property bool multipleLayouts: true
function refresh() { function refresh() {
if (!queryProc.running) queryProc.running = true if (!queryProc.running) queryProc.running = true
@@ -27,9 +31,11 @@ BarWidget {
return typed.find(k => k.main) ?? typed.find(k => k.name === root.keyboardName) return typed.find(k => k.main) ?? typed.find(k => k.name === root.keyboardName)
} }
// switchxkblayout is a hyprctl command rather than a dispatcher, so it has to
// be run rather than sent over the dispatch socket.
function cycleLayout() { function cycleLayout() {
if (!root.keyboardName) return if (!root.keyboardName || !root.bar) return
Hyprland.dispatch("switchxkblayout " + root.keyboardName + " next") root.bar.run("hyprctl switchxkblayout " + Util.shellQuote(root.keyboardName) + " next")
refreshTimer.restart() refreshTimer.restart()
} }
@@ -59,6 +65,7 @@ BarWidget {
if (!kb || !kb.active_keymap) return if (!kb || !kb.active_keymap) return
root.keyboardName = String(kb.name || "") root.keyboardName = String(kb.name || "")
root.multipleLayouts = kb.layout === undefined || String(kb.layout).indexOf(",") !== -1
root.layoutFull = kb.active_keymap root.layoutFull = kb.active_keymap
root.layoutLabel = kb.active_keymap.split(/\s+/)[0].substring(0, 3).toUpperCase() root.layoutLabel = kb.active_keymap.split(/\s+/)[0].substring(0, 3).toUpperCase()
} }
@@ -78,7 +85,7 @@ BarWidget {
onTriggered: root.refresh() onTriggered: root.refresh()
} }
visible: layoutLabel !== "" visible: layoutLabel !== "" && multipleLayouts
implicitWidth: button.implicitWidth implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight implicitHeight: button.implicitHeight
+7
View File
@@ -918,6 +918,13 @@ ShellRoot {
} }
} }
// Enable, but only where the widget is not on the bar already, so a caller
// that cannot know whether it ran before leaves a placed widget alone.
function putBarWidget(id: string, placementJson: string): string {
if (shell.pluginRegistry.inBar(id)) return "ok"
return enablePlugin(id, placementJson)
}
function moveBarWidget(id: string, placementJson: string): string { function moveBarWidget(id: string, placementJson: string): string {
try { try {
var error = shell.pluginRegistry.moveBarWidget(id, JSON.parse(placementJson || "{}")) var error = shell.pluginRegistry.moveBarWidget(id, JSON.parse(placementJson || "{}"))
+37
View File
@@ -328,3 +328,40 @@ jq -e 'all(.[]; .id != "omarchy.audio")' <<<"$geometry" >/dev/null || {
} }
pass "bar remove reloads shell config and updates bar layout" pass "bar remove reloads shell config and updates bar layout"
# 'bar put' is what migrations use to place a newly shipped widget, so it has
# to place one that is missing and leave one that is already there alone,
# however often it runs.
bar_put() {
HOME="$test_home" OMARCHY_PATH="$test_root" PATH="$ROOT/bin:$PATH" "$ROOT/bin/omarchy-bar" put "$@"
}
center_ids() {
jq -c '[.bar.layout.center[] | .id // .]' <<<"$(shell_ipc shell listShellConfig)"
}
bar_put omarchy.keyboard-layout --after omarchy.clock >/dev/null
for _ in {1..80}; do
[[ $(center_ids) == *omarchy.keyboard-layout* ]] && break
kill -0 "$QS_PID" 2>/dev/null || fail_with_log "test shell exited while putting a bar widget"
sleep 0.1
done
jq -e '
[.bar.layout.center[] | .id // .] as $ids
| ($ids | index("omarchy.clock")) as $clock
| ($ids | index("omarchy.keyboard-layout")) as $widget
| $clock != null and $widget == $clock + 1
' <<<"$(shell_ipc shell listShellConfig)" >/dev/null ||
fail_with_log "bar put places a widget after the one it names ($(center_ids))"
pass "bar put places a widget after the one it names"
placed=$(center_ids)
bar_put omarchy.keyboard-layout --section right >/dev/null
sleep 0.5
[[ $(center_ids) == "$placed" ]] ||
fail_with_log "bar put left a widget already on the bar alone (was $placed, now $(center_ids))"
jq -e 'all(.bar.layout.right[]; (.id // .) != "omarchy.keyboard-layout")' \
<<<"$(shell_ipc shell listShellConfig)" >/dev/null ||
fail_with_log "bar put added a second copy of a widget already on the bar"
pass "bar put leaves a widget already on the bar alone"