diff --git a/config/omarchy/shell.json b/config/omarchy/shell.json index a3f4c329..c746eef3 100644 --- a/config/omarchy/shell.json +++ b/config/omarchy/shell.json @@ -38,6 +38,9 @@ { "id": "omarchy.tray" }, + { + "id": "omarchy.model-usage" + }, { "id": "omarchy.bluetooth" }, diff --git a/migrations/1785344985.sh b/migrations/1785344985.sh new file mode 100644 index 00000000..96deab5d --- /dev/null +++ b/migrations/1785344985.sh @@ -0,0 +1,49 @@ +echo "Add the model usage widget to the bar" + +# The widget is now in the default layout. It draws nothing until a provider +# reports usage — Panel.qml is `visible: providers.length > 0`, and Main.qml +# only counts a provider that is enabled *and* has recorded prompts, sessions, +# active days, or a rate limit. So adding it to every existing bar is free: +# machines without Claude Code or Codex never see it. + +config_file="$HOME/.config/omarchy/shell.json" + +if [[ -s $config_file ]] && omarchy-cmd-present jq; then + tmp=$(mktemp) + jq ' + def entry_id: + if type == "string" then + . + elif type == "object" then + (.id // "") + else + "" + end; + + def has_widget($id): + [(.bar.layout // {}) | to_entries[] | .value | select(type == "array") | .[] | entry_id] + | any(. == $id); + + def insert_after($anchor; $entry): + if type != "array" then + [$entry] + else + ([range(0; length) as $i | select((.[$i] | entry_id) == $anchor) | $i][0]) as $anchor_index | + if $anchor_index == null then + [$entry] + . + else + .[0:$anchor_index + 1] + [$entry] + .[$anchor_index + 1:] + end + end; + + # Respect a bar the user already curated: only place the widget when it is + # absent from every section, never a second copy. + if has_widget("omarchy.model-usage") then + . + else + .bar.layout.right |= insert_after("omarchy.tray"; { id: "omarchy.model-usage" }) + end + ' "$config_file" >"$tmp" && mv "$tmp" "$config_file" || rm -f "$tmp" +fi + +omarchy-restart-shell diff --git a/shell/plugins/model-usage/README.md b/shell/plugins/model-usage/README.md index 59b224cf..09281b36 100644 --- a/shell/plugins/model-usage/README.md +++ b/shell/plugins/model-usage/README.md @@ -29,6 +29,11 @@ there is no switch row at all; with none, the module leaves the bar entirely rather than sitting there with nothing to say. A CLI installed mid-session shows up at the next refresh, so nothing polls the disk waiting for it. +That self-hiding is why the widget ships in the default bar layout: a machine +that has never run Claude Code or Codex draws nothing, and the icon arrives on +its own the first time a scan finds usage. Drop it with +`omarchy bar plugin remove omarchy.model-usage`. + ## Providers | Provider | Limits | Local stats | diff --git a/test/shell.d/model-usage-default-migration-test.sh b/test/shell.d/model-usage-default-migration-test.sh new file mode 100755 index 00000000..8250f3fc --- /dev/null +++ b/test/shell.d/model-usage-default-migration-test.sh @@ -0,0 +1,119 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "$0")/base-test.sh" + +require_command jq + +migration="$ROOT/migrations/1785344985.sh" +test_dir=$(mktemp -d) +trap 'rm -rf "$test_dir"' EXIT + +mkdir -p "$test_dir/bin" + +cat >"$test_dir/bin/omarchy-cmd-present" <<'STUB' +#!/bin/bash + +command -v "$1" >/dev/null +STUB + +cat >"$test_dir/bin/omarchy-restart-shell" <<'STUB' +#!/bin/bash + +echo restart >>"$SHELL_RESTARTS" +STUB + +chmod +x "$test_dir/bin/"* + +export SHELL_RESTARTS="$test_dir/shell-restarts" + +home="$test_dir/home" +config="$home/.config/omarchy/shell.json" + +run_migration() { + : >"$SHELL_RESTARTS" + HOME="$home" PATH="$test_dir/bin:$PATH" bash -euo pipefail "$migration" >/dev/null +} + +# The shipped default minus the widget is what every machine installed before +# this migration has on disk. +write_config() { + rm -rf "$home" + mkdir -p "$home/.config/omarchy" + jq "${1:-.}" "$ROOT/config/omarchy/shell.json" >"$config" +} + +without_widget='del(.bar.layout[][] | select((if type == "object" then .id else . end) == "omarchy.model-usage"))' + +ids() { + jq -c --arg section "$1" '[.bar.layout[$section][]? | if type == "object" then .id else . end]' "$config" +} + +# ------------------------------------------------------------------ shipped default + +jq -e '[.bar.layout.right[].id] | index("omarchy.model-usage")' "$ROOT/config/omarchy/shell.json" >/dev/null || + fail "shipped config puts model usage in the bar" +pass "shipped config puts model usage in the bar" + +# ------------------------------------------------------------------ placement + +write_config "$without_widget" +run_migration + +[[ $(ids right) == '["omarchy.tray","omarchy.model-usage","omarchy.bluetooth","omarchy.network","omarchy.audio","omarchy.monitor","omarchy.power"]' ]] || + fail "migration inserts model usage after the tray" "$(ids right)" +pass "migration inserts model usage after the tray" + +(($(wc -l <"$SHELL_RESTARTS") == 1)) || fail "migration restarts the shell" +pass "migration restarts the shell" + +before=$(sha256sum "$config") +run_migration +[[ $before == $(sha256sum "$config") ]] || fail "migration is idempotent" "$(ids right)" +pass "migration is idempotent" + +# ------------------------------------------------------------------ curated bars + +# A user who already placed the widget keeps it exactly where they put it, in +# whichever section, and never gets a second copy. +write_config "$without_widget | .bar.layout.center += [{ id: \"omarchy.model-usage\" }]" +run_migration + +[[ $(ids center) == *'"omarchy.model-usage"'* ]] || fail "migration leaves a user-placed widget alone" "$(ids center)" +[[ $(ids right) != *'"omarchy.model-usage"'* ]] || fail "migration does not add a second copy" "$(ids right)" +pass "migration respects a widget the user already placed" + +# Layouts written before entries grew options are bare id strings. +write_config "$without_widget | .bar.layout.right = [\"omarchy.tray\", \"omarchy.model-usage\", \"omarchy.power\"]" +run_migration + +[[ $(ids right) == '["omarchy.tray","omarchy.model-usage","omarchy.power"]' ]] || + fail "migration reads string-form entries" "$(ids right)" +pass "migration reads string-form entries" + +# A tray dropped from the right section must not strand the widget or drop it. +write_config "$without_widget | del(.bar.layout.right[] | select(.id == \"omarchy.tray\"))" +run_migration + +[[ $(ids right) == '["omarchy.model-usage",'* ]] || fail "migration places the widget without a tray" "$(ids right)" +pass "migration places the widget without a tray" + +# ------------------------------------------------------------------ everything else + +write_config "$without_widget" +cp "$config" "$test_dir/before.json" +run_migration + +diff <(jq -S 'del(.bar.layout.right)' "$test_dir/before.json") <(jq -S 'del(.bar.layout.right)' "$config") >/dev/null || + fail "migration touches nothing but the right section" "$(diff <(jq -S . "$test_dir/before.json") <(jq -S . "$config"))" +pass "migration touches nothing but the right section" + +# A config the migration cannot parse is left alone rather than truncated. +rm -rf "$home" +mkdir -p "$home/.config/omarchy" +printf '{ not json' >"$config" +run_migration + +[[ $(cat "$config") == '{ not json' ]] || fail "migration leaves an unparsable config untouched" "$(cat "$config")" +pass "migration leaves an unparsable config untouched"