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"