From e1d0c4e0a8807b48fb00548cf6f337adbd803ec6 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 9 Aug 2026 19:38:46 +0200 Subject: [PATCH] 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) * 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) * 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) * 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) * 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) * 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) * 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) * 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) * 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) * 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) --------- Co-authored-by: Claude Opus 5 (1M context) --- bin/omarchy-bar | 80 ++++++++++++++++---- config/omarchy/shell.json | 3 + migrations/1786279107.sh | 9 +++ shell/plugins/bar/widgets/KeyboardLayout.qml | 13 +++- shell/shell.qml | 7 ++ test/shell.d/runtime-smoke-test.sh | 37 +++++++++ 6 files changed, 132 insertions(+), 17 deletions(-) create mode 100644 migrations/1786279107.sh diff --git a/bin/omarchy-bar b/bin/omarchy-bar index bf2923ff..1ae63f3c 100755 --- a/bin/omarchy-bar +++ b/bin/omarchy-bar @@ -2,8 +2,8 @@ # omarchy:summary=Configure the bar and its widget layout # omarchy:group=bar -# omarchy:args=use | reset | defaults | position | transparent | move [placement] | set [--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:args=use | reset | defaults | position | transparent | put [placement] | move [placement] | set [--json] [placement] +# 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 @@ -18,6 +18,8 @@ Usage: omarchy bar [args...] defaults Restore the default bar and service widgets position Bar position transparent Bar transparency + put [placement] Put a widget on the bar, leaving one + that is already there where it is move [placement] Move a widget within or between sections set [--json] [placement] Set a per-widget option @@ -33,8 +35,12 @@ Placement: Enable and disable widgets with 'omarchy plugin enable' and '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: 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 bar set omarchy.clock format HH:mm @@ -152,22 +158,14 @@ cmd_use() { cmd_defaults() { (( $# == 0 )) || fail "defaults does not take arguments" - local catalog local optional_widgets="[]" local service widget - catalog=$(omarchy-plugin-catalog) for service in dropbox tailscale; do if "omarchy-installed-service-$service"; then - widget=$(jq -c --arg id "omarchy.$service" ' - map(select(.id == $id))[0] as $plugin - | { - id: $id, - section: ( - $plugin.barWidget.defaultSection // "center" - | if IN("left", "center", "right") then . else "center" end - ) - } - ' <<<"$catalog") + widget=$(jq -cn \ + --arg id "omarchy.$service" \ + --arg section "$(bar_widget_default_section "omarchy.$service")" \ + '{id: $id, section: $section}') optional_widgets=$(jq -c --argjson widget "$widget" '. + [$widget]' <<<"$optional_widgets") fi done @@ -219,6 +217,57 @@ cmd_transparent() { 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() { local id="${1:-}" [[ -n $id ]] || fail "move requires a widget id" @@ -302,6 +351,9 @@ case "$command" in transparent) cmd_transparent "$@" ;; + put) + cmd_put "$@" + ;; move) cmd_move "$@" ;; diff --git a/config/omarchy/shell.json b/config/omarchy/shell.json index 553a6b80..e6202da8 100644 --- a/config/omarchy/shell.json +++ b/config/omarchy/shell.json @@ -27,6 +27,9 @@ "formatAlt": "d MMMM 'W'ww yyyy", "verticalFormat": "HH\n\u2014\nmm" }, + { + "id": "omarchy.keyboard-layout" + }, { "id": "omarchy.weather" }, diff --git a/migrations/1786279107.sh b/migrations/1786279107.sh new file mode 100644 index 00000000..1b2e3d8f --- /dev/null +++ b/migrations/1786279107.sh @@ -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 diff --git a/shell/plugins/bar/widgets/KeyboardLayout.qml b/shell/plugins/bar/widgets/KeyboardLayout.qml index 979ff12b..67fba33b 100644 --- a/shell/plugins/bar/widgets/KeyboardLayout.qml +++ b/shell/plugins/bar/widgets/KeyboardLayout.qml @@ -13,6 +13,10 @@ BarWidget { property string layoutLabel: "" property string layoutFull: "" 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() { if (!queryProc.running) queryProc.running = true @@ -27,9 +31,11 @@ BarWidget { 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() { - if (!root.keyboardName) return - Hyprland.dispatch("switchxkblayout " + root.keyboardName + " next") + if (!root.keyboardName || !root.bar) return + root.bar.run("hyprctl switchxkblayout " + Util.shellQuote(root.keyboardName) + " next") refreshTimer.restart() } @@ -59,6 +65,7 @@ BarWidget { if (!kb || !kb.active_keymap) return root.keyboardName = String(kb.name || "") + root.multipleLayouts = kb.layout === undefined || String(kb.layout).indexOf(",") !== -1 root.layoutFull = kb.active_keymap root.layoutLabel = kb.active_keymap.split(/\s+/)[0].substring(0, 3).toUpperCase() } @@ -78,7 +85,7 @@ BarWidget { onTriggered: root.refresh() } - visible: layoutLabel !== "" + visible: layoutLabel !== "" && multipleLayouts implicitWidth: button.implicitWidth implicitHeight: button.implicitHeight diff --git a/shell/shell.qml b/shell/shell.qml index f6062a25..aa74ecba 100644 --- a/shell/shell.qml +++ b/shell/shell.qml @@ -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 { try { var error = shell.pluginRegistry.moveBarWidget(id, JSON.parse(placementJson || "{}")) diff --git a/test/shell.d/runtime-smoke-test.sh b/test/shell.d/runtime-smoke-test.sh index d1a7dc60..42d9746f 100755 --- a/test/shell.d/runtime-smoke-test.sh +++ b/test/shell.d/runtime-smoke-test.sh @@ -328,3 +328,40 @@ jq -e 'all(.[]; .id != "omarchy.audio")' <<<"$geometry" >/dev/null || { } 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"