Plugin cloning via menu (#6433)

* Add plugin cloning via menu

* Split plugin commands by action

* Keep plugin enablement in action commands

* Remove unused plugin edit command

* Simplify plugin rescan arguments

* Assume Omarchy shell is running for plugin commands

* Remove plugin compatibility dispatcher

* Flatten plugin clone command

* Keep only shared plugin helpers

* Remove plugin rescan wrapper

* Keep plugin commands self-contained

* Simplify plugin clone lifecycle
This commit is contained in:
David Heinemeier Hansson
2026-07-29 21:34:47 -04:00
committed by GitHub
parent fb564e3ac1
commit f8835df644
28 changed files with 1238 additions and 1012 deletions
+3 -2
View File
@@ -458,8 +458,9 @@ cmd_replace() {
prog=$(cat <<JQ
$JQ_DEFS $NORMALIZE
| resolve_source as \$source
| if \$source.entry | type != "object" then error("widget entry must be an object") else . end
| .bar.layout[\$source.section][\$source.index].id = \$new
| .bar.layout[\$source.section][\$source.index] = (
\$source.entry | if type == "object" then .id = \$new else { id: \$new } end
)
JQ
)
# resolve_source finds by widget id when no placement is given; replace
+15 -9
View File
@@ -1,10 +1,9 @@
#!/bin/bash
# omarchy:summary=Pick a shell plugin to enable, disable, or remove
# omarchy:summary=Pick a shell plugin to enable, disable, clone, or remove
# omarchy:group=menu
# omarchy:name=plugin
# omarchy:args=<enable|disable|remove>
# omarchy:examples=omarchy menu plugin enable | omarchy menu plugin disable
# omarchy:args=<enable|disable|clone|remove>
set -euo pipefail
@@ -13,19 +12,21 @@ PLUGIN_ICON=$'\U000f0431'
case "${1:-}" in
enable) filter='(.enabled | not)' ;;
disable) filter='.canDisable and .enabled' ;;
clone) filter='.firstParty and ((.id | sub("^omarchy\\."; "local.")) as $clone_id | ($plugins | map(.id) | index($clone_id)) == null)' ;;
remove) filter='(.firstParty | not)' ;;
*)
echo "Usage: omarchy-menu-plugin <enable|disable|remove>" >&2
echo "Usage: omarchy-menu-plugin <enable|disable|clone|remove>" >&2
exit 1
;;
esac
plugins=$(omarchy-plugin list --json) || exit 1
plugins=$(omarchy-plugin-list --json)
# Two plugins may share a name, so keep the id with the row and show it when the
# name alone cannot identify the pick.
rows=$(jq -r --arg icon "$PLUGIN_ICON" \
"([.[] | select($filter)]) as \$rows
". as \$plugins
| ([.[] | select($filter)]) as \$rows
| \$rows[]
| .name as \$name
| (if ([\$rows[] | select(.name == \$name)] | length) > 1 then \$name + \" (\" + .id + \")\" else \$name end) as \$label
@@ -38,8 +39,13 @@ name=$(omarchy-menu-select "${1^} plugin" < <(cut -f1,2 <<<"$rows")) || exit 0
id=$(awk -F'\t' -v label="$name" '$2 == label { print $3; exit }' <<<"$rows")
[[ -n $id ]] || exit 1
if [[ $1 == "remove" ]]; then
omarchy-launch-floating-terminal-with-presentation "omarchy-plugin remove $(printf '%q' "$id")"
if [[ $1 == "clone" ]]; then
new_id="local.${id#omarchy.}"
target="$HOME/.config/omarchy/plugins/$new_id"
omarchy-launch-floating-terminal-with-presentation \
"omarchy-plugin-clone $(printf '%q' "$id") && exec \$EDITOR $(printf '%q' "$target")"
elif [[ $1 == "remove" ]]; then
omarchy-launch-floating-terminal-with-presentation "omarchy-plugin-remove $(printf '%q' "$id")"
else
omarchy-plugin "$1" "$id"
"omarchy-plugin-$1" "$id"
fi
-554
View File
@@ -1,554 +0,0 @@
#!/bin/bash
# omarchy:summary=Manage Omarchy shell plugins and bar widgets
# omarchy:group=plugin
# omarchy:args=<list|rescan|enable|disable|add|update|remove|clone|edit|validate> [...]
# omarchy:examples=omarchy plugin list | omarchy plugin add https://github.com/acme/omarchy-weather.git | omarchy plugin update --all | omarchy plugin clone omarchy.clock local.clock
set -euo pipefail
PLUGINS_DIR="$HOME/.config/omarchy/plugins"
# Never let git block an unattended run on a credential or host-key prompt; fail
# fast instead so error paths can handle it.
export GIT_TERMINAL_PROMPT=0
export GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -oBatchMode=yes}"
usage() {
cat <<USAGE
Usage: omarchy-plugin <command> [args...]
Manage plugins:
list [--json] List discovered shell plugins
rescan Rescan ~/.config/omarchy/plugins
enable <id> [placement] Enable a plugin (a bar replaces the one in use)
disable <id> Disable a plugin
Install from git (a plugin is a git repo):
add [git-url] [--enable] [--yes] Clone a plugin repo into your plugins
update [id | --all] [--yes] Fetch, review the diff, fast-forward
remove [id] [--yes] Disable and delete an installed plugin
Plugins are unsandboxed code — review what you add and enable.
Make your own:
clone [source] [new-id] [options] Clone a built-in or user plugin
edit [id] Open a user plugin directory in a shell
validate <plugin-folder> Check a plugin's manifest (for authors)
Bar widgets are placed in the layout with 'omarchy bar plugin'.
Examples:
omarchy plugin add https://github.com/acme/omarchy-weather.git --enable
omarchy plugin update --all
omarchy plugin clone omarchy.clock local.clock --name "My Clock" --replace
omarchy plugin enable acme.weather --section right
USAGE
}
fail() {
echo "omarchy-plugin: $*" >&2
exit 1
}
interactive() {
[[ -t 0 && -t 1 ]]
}
# Yes/no prompt. Honours ASSUME_YES, and refuses in a non-interactive context so
# an agent must pass --yes deliberately rather than hang on a prompt.
ASSUME_YES=0
confirm() {
local prompt="$1"
(( ASSUME_YES )) && return 0
if interactive; then
gum confirm "$prompt"
return
fi
fail "refusing to continue without confirmation; pass --yes"
}
# Plugin ids become paths under PLUGINS_DIR that we mv/rm, so reject anything
# that could escape it (matches the id rules in omarchy-plugin-validate).
valid_plugin_id() {
[[ $1 =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ && $1 != *..* ]]
}
is_bar_option() {
omarchy-plugin-catalog | jq -e --arg id "$1" 'any(.[]; .id == $id and (.kinds | index("bar")))' >/dev/null
}
# Only one bar runs at a time, so enabling one replaces the one before it. Say
# that rather than "Enabled", which reads like one more thing switched on next
# to everything already there.
enabled_message() {
local id="$1"
if is_bar_option "$id"; then
echo "Now using $id as the bar"
else
echo "Enabled $id"
fi
}
# A bar widget lands on the right when it is enabled, which is rarely where it
# belongs. Ask once, here, rather than leaving a follow-up 'omarchy bar plugin
# move' as the only way to place it.
place_bar_widget() {
local id="$1" section default_section
interactive || return 0
(( ASSUME_YES )) && return 0
# A plugin that is also a bar has no place in the layout to ask about.
jq -e '(.kinds // []) | (index("bar") | not) and (index("bar-widget") != null)' \
"$PLUGINS_DIR/$id/manifest.json" >/dev/null 2>&1 || return 0
default_section=$(jq -r '.barWidget.defaultSection // "center"' "$PLUGINS_DIR/$id/manifest.json")
section=$(printf '%s\n' left center right |
gum choose --header="Place $id in which bar section?" --selected "$default_section") || return 0
[[ -n $section ]] || return 0
omarchy-bar-plugin move "$id" --section "$section" >/dev/null && echo "Placed $id in the $section section"
}
installed_plugin_ids() {
find "$PLUGINS_DIR" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) ! -name '.*' -printf '%f\n' 2>/dev/null | sort
}
plugin_id_manifest() {
omarchy-plugin-catalog | jq -r --arg id "$1" '
map(select(.id == $id))[0].manifestPath // empty
'
}
plugin_discovered() {
local id="$1" plugins
plugins=$(omarchy-shell shell listPlugins 2>/dev/null) || return 1
jq -e --arg id "$id" 'any(.[]; .id == $id)' <<<"$plugins" >/dev/null 2>&1
}
wait_for_plugin_discovery() {
local id="$1" attempt
for (( attempt = 0; attempt < 40; attempt++ )); do
plugin_discovered "$id" && return 0
sleep 0.05
done
return 1
}
print_plugins() {
local json="false"
while (( $# > 0 )); do
case "$1" in
--json)
json="true"
;;
-h | --help)
usage
exit 0
;;
*)
fail "unknown list option: $1"
;;
esac
shift
done
local plugins
plugins=$(omarchy-shell shell listPlugins) || fail "could not list plugins; is omarchy-shell running?"
if [[ $json == "true" ]]; then
printf '%s\n' "$plugins"
return
fi
jq -r '
sort_by(.id)[] |
[ .id,
(if .enabled then "enabled" else "disabled" end),
(if .firstParty then "first-party" else "third-party" end),
((.kinds // []) | join(",")),
(.name // "")
] | @tsv
' <<<"$plugins" | awk -F '\t' '
BEGIN { printf "%-32s %-9s %-11s %-18s %s\n", "ID", "STATE", "SOURCE", "KINDS", "NAME" }
{ printf "%-32s %-9s %-11s %-18s %s\n", $1, $2, $3, $4, $5 }
'
}
rescan_plugins() {
(( $# == 0 )) || fail "rescan does not take arguments"
omarchy-shell shell rescanPlugins || fail "could not rescan plugins; is omarchy-shell running?"
echo "Plugins rescanned"
}
plugin_enabled() {
local enabled="$1"
local id="${2:-}"
[[ -n $id ]] || fail "plugin id is required"
shift 2
if [[ $enabled != "true" ]] && (( $# > 0 )); then
fail "disable does not take placement options"
fi
# Enabling a bar writes bar.id and touches no layout, so a placement could
# only be applied to a widget that is not there. Refuse before the bar is
# switched, rather than switching it and failing on the move afterwards.
if [[ $enabled == "true" ]] && (( $# > 0 )) && is_bar_option "$id"; then
fail "'$id' is a bar; it replaces the bar in use rather than taking a place in one"
fi
omarchy-shell shell rescanPlugins >/dev/null 2>&1 || true
if [[ $enabled == "true" ]]; then
wait_for_plugin_discovery "$id" || fail "plugin '$id' was not discovered; is omarchy-shell running?"
fi
local result
result=$(omarchy-shell shell setPluginEnabled "$id" "$enabled") || fail "could not update plugin; is omarchy-shell running?"
[[ $result == "ok" ]] || fail "plugin '$id' is not known; run: omarchy plugin rescan"
if [[ $enabled == "true" ]] && (( $# > 0 )); then
omarchy-bar-plugin move "$id" "$@"
echo "Enabled and moved $id"
elif [[ $enabled == "true" ]]; then
enabled_message "$id"
else
echo "Disabled $id"
fi
}
# ---------------------------------------------------------------- add
plugin_add() {
local url="" enable_after=""
while (( $# > 0 )); do
case "$1" in
--enable) enable_after=true; shift ;;
--no-enable) enable_after=false; shift ;;
--yes | -y) ASSUME_YES=1; shift ;;
-h | --help) usage; return 0 ;;
-*) fail "unknown add option: $1" ;;
*)
[[ -z $url ]] || fail "unexpected argument: $1"
url="$1"; shift ;;
esac
done
if [[ -z $url ]]; then
interactive || fail "a git URL is required (e.g. omarchy plugin add https://github.com/acme/omarchy-weather.git)"
url=$(gum input --prompt "Git URL of the plugin repo: ") || fail "cancelled"
[[ -n $url ]] || fail "a git URL is required"
fi
if (( ! ASSUME_YES )); then
cat >&2 <<WARN
⚠️ Plugins run as arbitrary, unsandboxed code inside your long-lived
omarchy-shell process. Only add repos you trust, and review the code
before you enable it.
URL: $url
WARN
confirm "Clone and add this plugin?" || fail "aborted"
fi
mkdir -p "$PLUGINS_DIR"
# Clone into a dot-prefixed staging dir (invisible to the plugin scanner) so
# a plugin only ever appears under its manifest id, fully validated.
local stage="$PLUGINS_DIR/.add.tmp.$$"
rm -rf "$stage"
if ! git clone -- "$url" "$stage"; then
rm -rf "$stage"
fail "failed to clone $url"
fi
if ! omarchy-plugin-validate "$stage"; then
rm -rf "$stage"
fail "refusing to add: validation failed"
fi
local id existing_manifest
id=$(jq -r '.id' "$stage/manifest.json")
existing_manifest=$(plugin_id_manifest "$id") || {
rm -rf "$stage"
fail "could not inspect installed plugin ids"
}
if [[ -n $existing_manifest ]]; then
rm -rf "$stage"
fail "plugin id '$id' is already used by $existing_manifest"
fi
local target="$PLUGINS_DIR/$id"
if [[ -e $target || -L $target ]]; then
rm -rf "$stage"
fail "plugin '$id' is already installed; update it with: omarchy plugin update $id"
fi
mv "$stage" "$target"
echo "Added $id into $target"
omarchy-shell shell rescanPlugins >/dev/null 2>&1 || true
if [[ -z $enable_after ]]; then
if (( ASSUME_YES )) || ! interactive; then
enable_after=false
elif confirm "Enable '$id' now?"; then
enable_after=true
else
enable_after=false
fi
fi
if [[ $enable_after == true ]]; then
if wait_for_plugin_discovery "$id" && [[ $(omarchy-shell shell setPluginEnabled "$id" true) == "ok" ]]; then
enabled_message "$id"
place_bar_widget "$id"
else
echo "Could not enable $id (is omarchy-shell running?). Enable later with: omarchy plugin enable $id" >&2
fi
else
echo "Enable it later with: omarchy plugin enable $id"
fi
}
# ---------------------------------------------------------------- update
UPDATED_ANY=0
update_one() {
local id="$1"
local dir="$PLUGINS_DIR/$id"
if ! git -C "$dir" fetch --quiet origin HEAD; then
echo "omarchy-plugin: fetch failed for '$id'" >&2
return 1
fi
if [[ $(git -C "$dir" rev-parse HEAD) == $(git -C "$dir" rev-parse FETCH_HEAD) ]]; then
echo "$id is up to date."
return 0
fi
if (( ! ASSUME_YES )); then
echo "Changes for $id:"
if omarchy-cmd-present delta; then
git -C "$dir" diff HEAD FETCH_HEAD | delta --paging=never
else
git -C "$dir" diff HEAD FETCH_HEAD
fi
echo
confirm "Update $id?" || { echo "Skipped $id."; return 0; }
fi
if ! git -C "$dir" merge --ff-only FETCH_HEAD >/dev/null 2>&1; then
echo "omarchy-plugin: cannot fast-forward '$id'; you have local changes in $dir" >&2
return 1
fi
# An update is code the shell will run, same as an add: re-validate, and roll
# back to the pre-merge commit if upstream turned invalid.
if ! omarchy-plugin-validate "$dir"; then
git -C "$dir" reset --hard ORIG_HEAD >/dev/null
echo "omarchy-plugin: update of '$id' failed validation; rolled back" >&2
return 1
fi
echo "Updated $id."
UPDATED_ANY=1
}
plugin_update() {
local id="" all=0
while (( $# > 0 )); do
case "$1" in
--all | -a) all=1; shift ;;
--yes | -y) ASSUME_YES=1; shift ;;
-h | --help) usage; return 0 ;;
-*) fail "unknown update option: $1" ;;
*)
[[ -z $id ]] || fail "unexpected argument: $1"
id="$1"; shift ;;
esac
done
if (( all )) && [[ -n $id ]]; then
fail "pass either a plugin-id or --all, not both"
fi
[[ -d $PLUGINS_DIR ]] || fail "no plugins installed"
local -a targets=()
if [[ -n $id ]]; then
valid_plugin_id "$id" || fail "invalid plugin id '$id'"
[[ -d "$PLUGINS_DIR/$id" ]] || fail "plugin '$id' is not installed"
[[ -d "$PLUGINS_DIR/$id/.git" ]] || fail "plugin '$id' is not a git checkout, so there is nothing to pull from"
targets=("$id")
else
local dir
for dir in "$PLUGINS_DIR"/*/; do
[[ -d $dir/.git ]] || continue
targets+=("$(basename "$dir")")
done
if (( ${#targets[@]} == 0 )); then
echo "No git-managed plugins installed."
return 0
fi
fi
local rc=0
for id in "${targets[@]}"; do
update_one "$id" || rc=1
done
if (( UPDATED_ANY )); then
omarchy-shell shell rescanPlugins >/dev/null 2>&1 || true
fi
return $rc
}
# ---------------------------------------------------------------- remove
plugin_remove() {
local id=""
while (( $# > 0 )); do
case "$1" in
--yes | -y) ASSUME_YES=1; shift ;;
-h | --help) usage; return 0 ;;
-*) fail "unknown remove option: $1" ;;
*)
[[ -z $id ]] || fail "unexpected argument: $1"
id="$1"; shift ;;
esac
done
[[ -d $PLUGINS_DIR ]] || fail "no plugins installed"
if [[ -z $id ]]; then
interactive || fail "a plugin-id is required"
id=$(installed_plugin_ids | gum choose --header="Remove which plugin?") || fail "cancelled"
[[ -n $id ]] || fail "nothing selected"
fi
valid_plugin_id "$id" || fail "invalid plugin id '$id'"
local target="$PLUGINS_DIR/$id"
[[ -e $target || -L $target ]] || fail "plugin '$id' is not installed"
local was_enabled=""
local shell_plugins
if shell_plugins=$(omarchy-shell shell listPlugins 2>/dev/null) && [[ -n $shell_plugins ]]; then
was_enabled=$(jq -r --arg id "$id" '.[] | select(.id == $id) | .enabled' <<<"$shell_plugins" 2>/dev/null) || true
fi
if [[ -L $target ]]; then
confirm "Unlink '$id' (symlink -> $(readlink "$target"))?" || fail "aborted"
elif [[ -d $target/.git ]]; then
confirm "Delete '$id'? Its git repo remains upstream." || fail "aborted"
else
confirm "Remove '$id'? The folder will be backed up." || fail "aborted"
fi
[[ $was_enabled == "true" ]] && omarchy-shell shell setPluginEnabled "$id" false >/dev/null 2>&1 || true
if [[ -L $target ]]; then
# A dev symlink is just unlinked; the files it points at are left alone.
rm -f "$target"
echo "Unlinked $id."
elif [[ -d $target/.git ]]; then
rm -rf "$target"
echo "Removed $id."
else
# A hand-made plugin may be the user's only copy, so keep a backup.
local base="$PLUGINS_DIR/.${id}.bak.$(date -u +%Y%m%d%H%M%S)"
local backup="$base" n=1
while [[ -e $backup ]]; do backup="${base}-${n}"; n=$((n + 1)); done
mv "$target" "$backup" || fail "failed to move $target to backup"
echo "Removed $id. Backup at: $backup"
fi
omarchy-shell shell rescanPlugins >/dev/null 2>&1 || true
if [[ $was_enabled == "true" ]]; then
echo "Plugin was enabled and was unloaded from omarchy-shell."
fi
}
# ---------------------------------------------------------------- edit
plugin_edit() {
local id=""
while (( $# > 0 )); do
case "$1" in
-h | --help) usage; return 0 ;;
-*) fail "unknown edit option: $1" ;;
*)
[[ -z $id ]] || fail "unexpected argument: $1"
id="$1"; shift ;;
esac
done
if [[ -z $id ]]; then
interactive || fail "a plugin id is required"
id=$(installed_plugin_ids | gum choose --header="Edit which plugin?") || fail "cancelled"
[[ -n $id ]] || fail "nothing selected"
fi
valid_plugin_id "$id" || fail "invalid plugin id '$id'"
[[ $id != omarchy.* ]] || fail "$id is built in; clone it first with: omarchy plugin clone $id"
local dir="$PLUGINS_DIR/$id"
[[ -f $dir/manifest.json ]] || fail "no user plugin at $dir"
if interactive; then
cd "$dir"
exec "${SHELL:-bash}"
fi
printf '%s\n' "$dir"
}
command="${1:-}"
case "$command" in
list | ls)
shift
print_plugins "$@"
;;
rescan)
shift
rescan_plugins "$@"
;;
enable)
shift
plugin_enabled true "$@"
;;
disable)
shift
plugin_enabled false "$@"
;;
add | install)
shift
plugin_add "$@"
;;
update)
shift
plugin_update "$@"
;;
remove | rm)
shift
plugin_remove "$@"
;;
edit)
shift
plugin_edit "$@"
;;
clone)
shift
exec omarchy-plugin-clone "$@"
;;
validate)
shift
exec omarchy-plugin-validate "$@"
;;
-h | --help | help | "")
usage
;;
*)
fail "unknown command: $command"
;;
esac
+170
View File
@@ -0,0 +1,170 @@
#!/bin/bash
# omarchy:summary=Add a shell plugin from git
# omarchy:group=plugin
# omarchy:args=[git-url] [--enable] [--yes]
# omarchy:examples=omarchy plugin add https://github.com/acme/omarchy-weather.git --enable
# omarchy:alias=omarchy plugin install
set -euo pipefail
export GIT_TERMINAL_PROMPT=0
export GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -oBatchMode=yes}"
PLUGINS_DIR="$HOME/.config/omarchy/plugins"
ASSUME_YES=0
fail() {
echo "omarchy-plugin-add: $*" >&2
exit 1
}
interactive() {
[[ -t 0 && -t 1 ]]
}
confirm() {
local prompt="$1"
(( ASSUME_YES )) && return 0
if interactive; then
gum confirm "$prompt"
else
fail "refusing to continue without confirmation; pass --yes"
fi
}
place_bar_widget() {
local id="$1"
local section
local default_section
interactive || return 0
(( ASSUME_YES )) && return 0
jq -e '(.kinds // []) | (index("bar") | not) and (index("bar-widget") != null)' \
"$PLUGINS_DIR/$id/manifest.json" >/dev/null 2>&1 || return 0
default_section=$(jq -r '.barWidget.defaultSection // "center"' "$PLUGINS_DIR/$id/manifest.json")
section=$(printf '%s\n' left center right |
gum choose --header="Place $id in which bar section?" --selected "$default_section") || return 0
[[ -n $section ]] || return 0
omarchy-bar-plugin move "$id" --section "$section" >/dev/null &&
echo "Placed $id in the $section section"
}
plugin_id_manifest() {
omarchy-plugin-catalog | jq -r --arg id "$1" '
map(select(.id == $id))[0].manifestPath // empty
'
}
url=""
enable_after=""
while (( $# > 0 )); do
case "$1" in
--enable)
enable_after=true
shift
;;
--yes | -y)
ASSUME_YES=1
shift
;;
-h | --help)
echo "Usage: omarchy plugin add [git-url] [--enable] [--yes]"
exit 0
;;
-*)
fail "unknown add option: $1"
;;
*)
[[ -z $url ]] || fail "unexpected argument: $1"
url="$1"
shift
;;
esac
done
if [[ -z $url ]]; then
interactive ||
fail "a git URL is required (e.g. omarchy plugin add https://github.com/acme/omarchy-weather.git)"
url=$(gum input --prompt "Git URL of the plugin repo: ") || fail "cancelled"
[[ -n $url ]] || fail "a git URL is required"
fi
if (( ! ASSUME_YES )); then
cat >&2 <<WARN
⚠️ Plugins run as arbitrary, unsandboxed code inside your long-lived
omarchy-shell process. Only add repos you trust, and review the code
before you enable it.
URL: $url
WARN
confirm "Clone and add this plugin?" || fail "aborted"
fi
mkdir -p "$PLUGINS_DIR"
stage="$PLUGINS_DIR/.add.tmp.$$"
rm -rf "$stage"
if ! git clone -- "$url" "$stage"; then
rm -rf "$stage"
fail "failed to clone $url"
fi
if ! omarchy-plugin-validate "$stage"; then
rm -rf "$stage"
fail "refusing to add: validation failed"
fi
id=$(jq -r '.id' "$stage/manifest.json")
existing_manifest=$(plugin_id_manifest "$id") || {
rm -rf "$stage"
fail "could not inspect installed plugin ids"
}
if [[ -n $existing_manifest ]]; then
rm -rf "$stage"
fail "plugin id '$id' is already used by $existing_manifest"
fi
target="$PLUGINS_DIR/$id"
if [[ -e $target || -L $target ]]; then
rm -rf "$stage"
fail "plugin '$id' is already installed; update it with: omarchy plugin update $id"
fi
mv "$stage" "$target"
echo "Added $id into $target"
omarchy-shell shell rescanPlugins >/dev/null
if [[ -z $enable_after ]]; then
if (( ASSUME_YES )) || ! interactive; then
enable_after=false
elif confirm "Enable '$id' now?"; then
enable_after=true
else
enable_after=false
fi
fi
if [[ $enable_after == true ]]; then
for (( attempt = 0; attempt < 40; attempt++ )); do
result=$(omarchy-shell shell setPluginEnabled "$id" true)
[[ $result == "ok" ]] && break
sleep 0.05
done
[[ $result == "ok" ]] || fail "plugin '$id' is not known"
if omarchy-plugin-catalog | jq -e --arg id "$id" '
any(.[]; .id == $id and (.kinds | index("bar")))
' >/dev/null; then
echo "Now using $id as the bar"
else
echo "Enabled $id"
fi
place_bar_widget "$id"
else
echo "Enable it later with: omarchy plugin enable $id"
fi
+3 -7
View File
@@ -14,15 +14,11 @@
set -o pipefail
OMARCHY_PATH="${OMARCHY_PATH:-}"
paths=()
if [[ -n $OMARCHY_PATH && -d $OMARCHY_PATH/shell/plugins ]]; then
while IFS= read -r manifest; do
paths+=("$manifest")
done < <(find "$OMARCHY_PATH/shell/plugins" -mindepth 2 -maxdepth 4 -type f \( -name manifest.json -o -name '*.manifest.json' \) 2>/dev/null | sort)
fi
while IFS= read -r manifest; do
paths+=("$manifest")
done < <(find "$OMARCHY_PATH/shell/plugins" -mindepth 2 -maxdepth 4 -type f \( -name manifest.json -o -name '*.manifest.json' \) | sort)
user_dir="$HOME/.config/omarchy/plugins"
if [[ -d $user_dir ]]; then
+156 -334
View File
@@ -1,9 +1,8 @@
#!/bin/bash
# omarchy:summary=Clone a built-in or user Omarchy shell plugin into your own config
# omarchy:summary=Clone a built-in Omarchy shell plugin into your own config
# omarchy:group=plugin
# omarchy:args=[source-id] [new-id] [--name <name>] [--replace] [--add [placement]] [--use]
# omarchy:examples=omarchy plugin clone | omarchy plugin clone omarchy.clock local.clock | omarchy plugin clone omarchy.clock local.clock --name "My Clock" --replace
# omarchy:args=<source-id>
set -euo pipefail
@@ -14,355 +13,178 @@ fail() {
exit 1
}
require_omarchy_path() {
[[ -n ${OMARCHY_PATH:-} ]] || fail "OMARCHY_PATH is not set"
copy_plugin() {
local source_dir="$1"
local manifest="$2"
local target_dir="$3"
if [[ ${manifest##*/} == "manifest.json" ]]; then
cp -aL "$source_dir/." "$target_dir/"
return
fi
cp -aL "$manifest" "$target_dir/manifest.json"
local source target source_pattern file
while IFS=$'\t' read -r source target; do
[[ $target != /* && $target != *".."* ]] || fail "invalid clone target path: $target"
if [[ -d $source_dir/$source ]]; then
mkdir -p "$target_dir/$target"
cp -aL "$source_dir/$source/." "$target_dir/$target/"
else
mkdir -p "$(dirname "$target_dir/$target")"
cp -aL "$source_dir/$source" "$target_dir/$target"
fi
if [[ $source != "$target" ]]; then
source_pattern=${source//./\\.}
while IFS= read -r -d '' file; do
sed -i "s|$source_pattern|$target|g" "$file"
done < <(rg --files-with-matches --null --fixed-strings "$source" "$target_dir")
fi
done < <(jq -r '
[
(.entryPoints[] | {source: ., target: .}),
(.omarchy.clonePaths[]? | {source: .source, target: .target})
] | unique_by(.target)[] | [.source, .target] | @tsv
' "$manifest")
}
interactive() {
[[ -t 0 && -t 1 ]]
update_manifest() {
local target_dir="$1"
local source_id="$2"
local new_id="$3"
local display_name="$4"
local manifest="$target_dir/manifest.json"
# Keep built-in ids inside the plugin code as stable IPC targets. The shell
# uses clonedFrom to route those calls to this local manifest id.
jq \
--arg id "$new_id" \
--arg name "$display_name" \
--arg sourceId "$source_id" '
.id = $id |
.name = $name |
if (.barWidget | type) == "object" then
.barWidget.displayName = $name
else
.
end |
.omarchy = (
(if (.omarchy | type) == "object" then .omarchy else {} end) +
{ clonedFrom: $sourceId }
) |
del(.omarchy.clonePaths)
' "$manifest" >"$manifest.tmp"
mv "$manifest.tmp" "$manifest"
}
slug_id() {
tr '[:upper:]' '[:lower:]' <<<"$1" | sed -E 's/^omarchy\.//; s/[^a-z0-9]+/-/g; s/^-+//; s/-+$//'
}
switch_to_clone() {
local source_id="$1"
local new_id="$2"
local is_bar="$3"
local is_bar_widget="$4"
local has_non_widget_kind="$5"
validate_plugin_id() {
local id="$1"
[[ $id =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || fail "plugin id must contain only letters, numbers, dots, underscores, and dashes"
[[ $id == *.* ]] || fail "plugin id must be namespaced, e.g. local.clock"
[[ $id != omarchy.* ]] || fail "plugin ids beginning with omarchy. are reserved for built-ins"
}
catalog_json() {
require_omarchy_path
omarchy-plugin-catalog
}
# Emit: id<TAB>barWidgetPath<TAB>name<TAB>category<TAB>allowMultiple for a
# built-in bar widget matching $1 (by id or alias). Used to find the source QML
# file and metadata to copy when cloning a widget.
builtin_widget_info() {
catalog_json | jq -r --arg id "$1" '
.[] | select((.kinds | index("bar-widget")) and .barWidgetPath != null)
| select(.id == $id or (((.barWidget.aliases // [])) | index($id)))
| [.id, .barWidgetPath, (.barWidget.displayName // .name // .id), (.barWidget.category // "Plugin"), (if .barWidget.allowMultiple == true then "true" else "false" end)]
| @tsv
' | head -1
}
clone_source_options() {
catalog_json | jq -r '
.[] | "\(.id)\t" +
(if .firstParty then (if (.kinds | index("bar-widget")) then "Built-in widget" else "Built-in plugin" end) else "User plugin" end) +
"\t" + ((.barWidget.displayName // .name // .id) // .id)
'
}
choose_clone_source() {
local selected
selected=$(clone_source_options | awk -F '\t' '{ printf "%-32s %-15s %s\n", $3, $2, $1 }' \
| gum filter --header "Clone plugin" --placeholder "Search built-in and user plugins..." --limit 1) || return 1
awk '{ print $NF }' <<<"$selected"
}
input_value() {
local prompt="$1"
local value="${2:-}"
gum input --prompt "$prompt " --value "$value"
}
rewrite_cloned_qml() {
local file="$1"
local id="$2"
local source_path="$3"
python3 - "$file" "$id" "$source_path" <<'PY'
import pathlib
import re
import sys
path, plugin_id, source_path = sys.argv[1], sys.argv[2], pathlib.Path(sys.argv[3])
text = open(path, encoding="utf-8").read()
text = re.sub(r'moduleName:\s*"[^"]+"', f'moduleName: "{plugin_id}"', text, count=1)
def resolve_relative(match):
rel = match.group(1)
if rel.startswith(("../", "./")) or (not rel.startswith("/") and "://" not in rel):
resolved = (source_path.parent / rel).resolve()
return '"' + resolved.as_uri() + '"'
return match.group(0)
text = re.sub(r'Qt\.resolvedUrl\("([^"]+)"\)', resolve_relative, text)
# A widget that imports a sibling JS module (the clock's Model.js) clones into
# a directory that does not have it. Point the import back at the bundled file,
# the same way Qt.resolvedUrl() references above are rewritten.
def resolve_js_import(match):
rel = match.group(1)
if rel.startswith("/") or "://" in rel:
return match.group(0)
resolved = (source_path.parent / rel).resolve()
return 'import "' + resolved.as_uri() + '"'
text = re.sub(r'import\s+"([^"]+\.js)"', resolve_js_import, text)
# Indicators dynamically loads sibling indicator components via string
# concatenation, so the simple Qt.resolvedUrl("literal") rewrite above cannot
# see it. Point the clone back at Omarchy's bundled indicator directory.
if source_path.name == "Indicators.qml":
indicators_url = (source_path.parent.parent / "indicators").resolve().as_uri() + "/"
text = text.replace(
'Qt.resolvedUrl("../indicators/" + indicatorSlot.indicatorId + ".qml")',
f'"{indicators_url}" + indicatorSlot.indicatorId + ".qml"',
)
# Panel-backed clones should not register the same global IPC target as the
# built-in widget. Direct panel clones can disable Panel.manageIpc themselves;
# wrapper widgets such as Weather disable it on the loaded panel instance.
if "ipcTarget:" in text and "manageIpc:" not in text:
text = text.replace(" id: root\n", " id: root\n manageIpc: false\n", 1)
if "function injectPanel()" in text and '"manageIpc" in target' not in text:
text = text.replace(
" if (!target) return\n",
" if (!target) return\n if (\"manageIpc\" in target) target.manageIpc = false\n",
1,
)
open(path, "w", encoding="utf-8").write(text)
PY
}
write_clone_manifest() {
local file="$1"
local id="$2"
local name="$3"
local description="$4"
local category="$5"
local multiple="$6"
local source_id="$7"
local source_path="$8"
python3 - "$file" "$id" "$name" "$description" "$category" "$multiple" "$source_id" "$source_path" <<'PY'
import json
import sys
file, plugin_id, name, description, category, multiple, source_id, source_path = sys.argv[1:]
data = {
"schemaVersion": 1,
"id": plugin_id,
"name": name,
"version": "1.0.0",
"author": "Local",
"description": description,
"kinds": ["bar-widget"],
"entryPoints": {"barWidget": "Widget.qml"},
"barWidget": {
"displayName": name,
"description": description,
"category": category,
"allowMultiple": multiple == "true"
},
"omarchy": {
"clonedFrom": source_id,
"clonedFromPath": source_path
}
}
open(file, "w", encoding="utf-8").write(json.dumps(data, indent=2) + "\n")
PY
}
update_cloned_manifest() {
local file="$1"
local id="$2"
local name="$3"
local source_id="$4"
local source_path="$5"
python3 - "$file" "$id" "$name" "$source_id" "$source_path" <<'PY'
import json
import sys
file, plugin_id, name, source_id, source_path = sys.argv[1:]
with open(file, encoding="utf-8") as handle:
data = json.load(handle)
data["id"] = plugin_id
data["name"] = name
if "barWidget" in data and isinstance(data["barWidget"], dict):
data["barWidget"]["displayName"] = name
meta = data.get("omarchy") if isinstance(data.get("omarchy"), dict) else {}
meta.update({"clonedFrom": source_id, "clonedFromPath": source_path})
data["omarchy"] = meta
open(file, "w", encoding="utf-8").write(json.dumps(data, indent=2) + "\n")
PY
if [[ $is_bar == "true" ]]; then
omarchy-bar use "$new_id"
elif [[ $is_bar_widget == "true" ]]; then
local shell_config
shell_config=$(omarchy-shell shell listShellConfig)
if jq -e --arg id "$source_id" '
[
.bar.layout.left[]?,
.bar.layout.center[]?,
.bar.layout.right[]?
] | any(.[]; (if type == "object" then .id else . end) == $id)
' <<<"$shell_config" >/dev/null; then
omarchy-bar-plugin replace "$source_id" "$new_id"
else
omarchy-bar-plugin add "$new_id"
fi
[[ $has_non_widget_kind == "false" ]] || omarchy-plugin-disable "$source_id"
else
omarchy-plugin-disable "$source_id"
omarchy-plugin-enable "$new_id"
fi
}
usage() {
cat <<USAGE
Usage: omarchy plugin clone [source-id] [new-id] [options]
Usage: omarchy plugin clone <source-id>
Clones a built-in or user plugin into ~/.config/omarchy/plugins/<new-id>/ so you
can edit it freely. The original stays built into Omarchy.
Copies a built-in plugin into ~/.config/omarchy/plugins/local.<id>/ so you can
edit it freely. The clone keeps all of the source plugin's files and kinds and
uses "My <name>" as its display name, then replaces the built-in with the clone.
Options:
--name <name> Display name for the clone
--replace Replace the first bar instance of the source with the clone
--add [placement] Add the cloned bar widget to the bar
--use Use the clone as the active bar option (bar plugins)
Placement (with --add):
--section <left|center|right>
--index <n>
--before <id>
--after <id>
Examples:
omarchy plugin clone
omarchy plugin clone omarchy.clock local.clock
omarchy plugin clone omarchy.clock local.clock --name "My Clock" --replace
USAGE
}
clone_command() {
require_omarchy_path
source_id=""
if (( $# > 0 )) && [[ $1 != --* ]]; then
source_id="$1"
shift
fi
local source_id=""
local new_id=""
if (( $# > 0 )) && [[ $1 != --* ]]; then
source_id="$1"
shift
fi
if (( $# > 0 )) && [[ $1 != --* ]]; then
new_id="$1"
shift
fi
while (( $# > 0 )); do
case "$1" in
-h | --help)
usage
exit 0
;;
*)
fail "unknown clone option: $1"
;;
esac
done
local display_name=""
local replace="false"
local add="false"
local use_bar="false"
local placement=()
[[ -n $source_id ]] || fail "clone source is required"
while (( $# > 0 )); do
case "$1" in
--name)
display_name="${2:-}"
[[ -n $display_name ]] || fail "--name requires a value"
shift 2
;;
--replace)
replace="true"
shift
;;
--add)
add="true"
shift
;;
--use)
use_bar="true"
shift
;;
--section | --index | --before | --after)
placement+=("$1" "${2:-}")
shift 2
;;
-h | --help)
usage
return
;;
*)
fail "unknown clone option: $1"
;;
esac
done
source_info=$(omarchy-plugin-catalog | jq -r --arg id "$source_id" '
.[] | select(.firstParty and .id == $id)
| [
.sourceDir,
.manifestPath,
(.name // .id),
((.kinds | index("bar")) != null),
((.kinds | index("bar-widget")) != null),
(any(.kinds[]; . != "bar-widget"))
] | @tsv
')
[[ -n $source_info ]] || fail "unknown built-in plugin: $source_id"
IFS=$'\t' read -r source_dir source_manifest source_name is_bar is_bar_widget has_non_widget_kind <<<"$source_info"
if [[ -z $source_id ]]; then
if interactive; then
source_id=$(choose_clone_source) || fail "clone cancelled"
else
fail "clone source is required"
fi
fi
new_id="local.${source_id#omarchy.}"
display_name="My $source_name"
target_dir="$PLUGINS_DIR/$new_id"
[[ ! -e $target_dir && ! -L $target_dir ]] || fail "$target_dir already exists"
local default_slug
default_slug=$(slug_id "$source_id")
if [[ -z $new_id ]]; then
if interactive; then
new_id=$(input_value "New plugin id:" "local.$default_slug")
else
fail "new plugin id is required"
fi
fi
validate_plugin_id "$new_id"
local target_dir="$PLUGINS_DIR/$new_id"
[[ ! -e $target_dir && ! -L $target_dir ]] || fail "$target_dir already exists"
local source_path=""
local source_name=""
local source_category="Plugin"
local source_multiple="false"
local source_type=""
if IFS=$'\t' read -r _ source_path source_name source_category source_multiple < <(builtin_widget_info "$source_id"); then
source_type="builtin-widget"
elif [[ -f $PLUGINS_DIR/$source_id/manifest.json ]]; then
source_type="user-plugin"
source_path="$PLUGINS_DIR/$source_id"
source_name=$(jq -r '.name // .id' "$source_path/manifest.json")
else
# Non-widget built-in plugin (e.g. a panel or bar): find it in the catalog.
local hit
hit=$(catalog_json | jq -r --arg id "$source_id" '.[] | select(.id == $id) | .sourceDir' | head -1)
if [[ -n $hit ]]; then
source_type="builtin-plugin"
source_path="$hit"
source_name=$(jq -r '.name // .id' "$source_path/manifest.json")
fi
fi
[[ -n $source_type ]] || fail "unknown clone source: $source_id"
if [[ -z $display_name ]]; then
if interactive; then
display_name=$(input_value "Display name:" "${source_name:-$new_id}")
else
display_name="${source_name:-$new_id}"
fi
fi
[[ -n $display_name ]] || fail "display name is required"
mkdir -p "$target_dir"
if [[ $source_type == "builtin-widget" ]]; then
cp "$source_path" "$target_dir/Widget.qml"
rewrite_cloned_qml "$target_dir/Widget.qml" "$new_id" "$source_path"
write_clone_manifest "$target_dir/manifest.json" "$new_id" "$display_name" "Cloned from $source_id" "$source_category" "$source_multiple" "$source_id" "$source_path"
else
cp -aL "$source_path/." "$target_dir/"
update_cloned_manifest "$target_dir/manifest.json" "$new_id" "$display_name" "$source_id" "$source_path"
fi
{
printf '# %s\n\n' "$display_name"
printf 'Cloned from `%s`.\n\n' "$source_id"
printf 'Source: `%s`\n\n' "$source_path"
printf 'The original remains built into Omarchy. Remove this plugin directory or swap\n'
printf 'your bar entry back to `%s` whenever you want to return to the built-in.\n' "$source_id"
} >"$target_dir/UPSTREAM.md"
omarchy-shell -q shell rescanPlugins >/dev/null 2>&1 || true
if [[ $replace == "true" ]]; then
omarchy-bar-plugin replace "$source_id" "$new_id"
echo "Cloned $source_id to $new_id and replaced the first bar instance"
elif [[ $add == "true" ]]; then
omarchy-bar-plugin add "$new_id" "${placement[@]}"
echo "Cloned $source_id to $new_id"
elif [[ $use_bar == "true" ]]; then
omarchy-bar use "$new_id"
echo "Cloned $source_id to $new_id and selected it as the active bar option"
else
echo "Cloned $source_id to $new_id"
fi
if interactive; then
cd "$target_dir"
exec "${SHELL:-bash}"
fi
mkdir -p "$PLUGINS_DIR"
stage=$(mktemp -d "$PLUGINS_DIR/.clone.XXXXXX")
clone_complete=0
cleanup() {
[[ -d ${stage:-} ]] && rm -rf "$stage"
(( clone_complete )) || rm -rf "$target_dir"
}
trap cleanup EXIT
copy_plugin "$source_dir" "$source_manifest" "$stage"
update_manifest "$stage" "$source_id" "$new_id" "$display_name"
mv "$stage" "$target_dir"
stage=""
clone_command "$@"
omarchy-shell shell rescanPlugins >/dev/null
discovered=0
for (( attempt = 0; attempt < 40; attempt++ )); do
if omarchy-plugin-list --json | jq -e --arg id "$new_id" 'any(.[]; .id == $id)' >/dev/null; then
discovered=1
break
fi
sleep 0.05
done
(( discovered )) || fail "cloned plugin '$new_id' was not discovered"
switch_to_clone "$source_id" "$new_id" "$is_bar" "$is_bar_widget" "$has_non_widget_kind"
clone_complete=1
omarchy-notification-send -g 󰐱 \
"Editing Cloned Plugin" \
"Original plugin has been replace by clone."
echo "Cloned $source_id to $target_dir and switched to $new_id"
+26
View File
@@ -0,0 +1,26 @@
#!/bin/bash
# omarchy:summary=Disable a shell plugin
# omarchy:group=plugin
# omarchy:args=<id>
set -euo pipefail
fail() {
echo "omarchy-plugin-disable: $*" >&2
exit 1
}
if [[ ${1:-} == "-h" || ${1:-} == "--help" ]]; then
echo "Usage: omarchy plugin disable <id>"
exit 0
fi
id="${1:-}"
[[ -n $id ]] || fail "plugin id is required"
result=$(omarchy-shell shell setPluginEnabled "$id" false)
[[ $result == "ok" ]] ||
fail "plugin '$id' is not known; run: omarchy-shell shell rescanPlugins"
echo "Disabled $id"
+43
View File
@@ -0,0 +1,43 @@
#!/bin/bash
# omarchy:summary=Enable a shell plugin
# omarchy:group=plugin
# omarchy:args=<id> [placement]
# omarchy:examples=omarchy plugin enable acme.weather --section right
set -euo pipefail
fail() {
echo "omarchy-plugin-enable: $*" >&2
exit 1
}
if [[ ${1:-} == "-h" || ${1:-} == "--help" ]]; then
echo "Usage: omarchy plugin enable <id> [placement]"
exit 0
fi
id="${1:-}"
[[ -n $id ]] || fail "plugin id is required"
shift
if (( $# > 0 )) && omarchy-plugin-catalog | jq -e --arg id "$id" '
any(.[]; .id == $id and (.kinds | index("bar")))
' >/dev/null; then
fail "'$id' is a bar; it replaces the bar in use rather than taking a place in one"
fi
result=$(omarchy-shell shell setPluginEnabled "$id" true)
[[ $result == "ok" ]] ||
fail "plugin '$id' is not known; run: omarchy-shell shell rescanPlugins"
if (( $# > 0 )); then
omarchy-bar-plugin move "$id" "$@"
echo "Enabled and moved $id"
elif omarchy-plugin-catalog | jq -e --arg id "$id" '
any(.[]; .id == $id and (.kinds | index("bar")))
' >/dev/null; then
echo "Now using $id as the bar"
else
echo "Enabled $id"
fi
+46
View File
@@ -0,0 +1,46 @@
#!/bin/bash
# omarchy:summary=List discovered shell plugins
# omarchy:group=plugin
# omarchy:args=[--json]
set -euo pipefail
json="false"
while (( $# > 0 )); do
case "$1" in
--json)
json="true"
;;
-h | --help)
echo "Usage: omarchy plugin list [--json]"
exit 0
;;
*)
echo "omarchy-plugin-list: unknown option: $1" >&2
exit 1
;;
esac
shift
done
plugins=$(omarchy-shell shell listPlugins)
if [[ $json == "true" ]]; then
printf '%s\n' "$plugins"
exit 0
fi
jq -r '
.[] |
[ .id,
(if .enabled then "enabled" else "disabled" end),
(if .firstParty then "first-party" else "third-party" end),
((.kinds // []) | join(",")),
(.name // "")
] | @tsv
' <<<"$plugins" | awk -F '\t' '
BEGIN { printf "%-32s %-9s %-11s %-18s %s\n", "ID", "STATE", "SOURCE", "KINDS", "NAME" }
{ printf "%-32s %-9s %-11s %-18s %s\n", $1, $2, $3, $4, $5 }
'
+141
View File
@@ -0,0 +1,141 @@
#!/bin/bash
# omarchy:summary=Remove an installed shell plugin
# omarchy:group=plugin
# omarchy:args=[id] [--yes]
# omarchy:alias=omarchy plugin rm
set -euo pipefail
PLUGINS_DIR="$HOME/.config/omarchy/plugins"
ASSUME_YES=0
fail() {
echo "omarchy-plugin-remove: $*" >&2
exit 1
}
interactive() {
[[ -t 0 && -t 1 ]]
}
confirm() {
local prompt="$1"
(( ASSUME_YES )) && return 0
if interactive; then
gum confirm "$prompt"
else
fail "refusing to continue without confirmation; pass --yes"
fi
}
valid_plugin_id() {
[[ $1 =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ && $1 != *..* ]]
}
id=""
while (( $# > 0 )); do
case "$1" in
--yes | -y)
ASSUME_YES=1
shift
;;
-h | --help)
echo "Usage: omarchy plugin remove [id] [--yes]"
exit 0
;;
-*)
fail "unknown remove option: $1"
;;
*)
[[ -z $id ]] || fail "unexpected argument: $1"
id="$1"
shift
;;
esac
done
[[ -d $PLUGINS_DIR ]] || fail "no plugins installed"
if [[ -z $id ]]; then
interactive || fail "a plugin-id is required"
id=$(find "$PLUGINS_DIR" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) \
! -name '.*' -printf '%f\n' 2>/dev/null |
sort |
gum choose --header="Remove which plugin?") || fail "cancelled"
[[ -n $id ]] || fail "nothing selected"
fi
valid_plugin_id "$id" || fail "invalid plugin id '$id'"
target="$PLUGINS_DIR/$id"
[[ -e $target || -L $target ]] || fail "plugin '$id' is not installed"
was_enabled=""
shell_plugins=$(omarchy-shell shell listPlugins)
if [[ -n $shell_plugins ]]; then
was_enabled=$(jq -r --arg id "$id" '
.[] | select(.id == $id) | .enabled
' <<<"$shell_plugins")
fi
cloned_from=""
restored_source=0
if [[ -f $target/manifest.json ]]; then
cloned_from=$(jq -r '.omarchy.clonedFrom // empty' "$target/manifest.json")
fi
if [[ -L $target ]]; then
confirm "Unlink '$id' (symlink -> $(readlink "$target"))?" || fail "aborted"
elif [[ -d $target/.git ]]; then
confirm "Delete '$id'? Its git repo remains upstream." || fail "aborted"
else
confirm "Remove '$id'? The folder will be backed up." || fail "aborted"
fi
if [[ -n $cloned_from ]]; then
is_bar=$(jq -r '(.kinds | index("bar")) != null' "$target/manifest.json")
is_bar_widget=$(jq -r '(.kinds | index("bar-widget")) != null' "$target/manifest.json")
has_non_widget_kind=$(jq -r 'any(.kinds[]; . != "bar-widget")' "$target/manifest.json")
if [[ $is_bar == "true" && $was_enabled == "true" ]]; then
omarchy-bar use "$cloned_from"
restored_source=1
elif [[ $is_bar_widget == "true" && $was_enabled == "true" ]]; then
omarchy-bar-plugin replace "$id" "$cloned_from"
[[ $has_non_widget_kind == "false" ]] || omarchy-plugin-enable "$cloned_from"
restored_source=1
elif [[ $has_non_widget_kind == "true" ]]; then
[[ $was_enabled != "true" ]] || omarchy-shell shell setPluginEnabled "$id" false >/dev/null
omarchy-plugin-enable "$cloned_from"
restored_source=1
fi
elif [[ $was_enabled == "true" ]]; then
omarchy-shell shell setPluginEnabled "$id" false >/dev/null
fi
if [[ -L $target ]]; then
rm -f "$target"
echo "Unlinked $id."
elif [[ -d $target/.git ]]; then
rm -rf "$target"
echo "Removed $id."
else
base="$PLUGINS_DIR/.${id}.bak.$(date -u +%Y%m%d%H%M%S)"
backup="$base"
n=1
while [[ -e $backup ]]; do
backup="${base}-${n}"
n=$((n + 1))
done
mv "$target" "$backup" || fail "failed to move $target to backup"
echo "Removed $id. Backup at: $backup"
fi
omarchy-shell shell rescanPlugins >/dev/null
if (( restored_source )); then
echo "Restored $cloned_from."
elif [[ $was_enabled == "true" ]]; then
echo "Plugin was enabled and was unloaded from omarchy-shell."
fi
+133
View File
@@ -0,0 +1,133 @@
#!/bin/bash
# omarchy:summary=Update installed git-managed plugins
# omarchy:group=plugin
# omarchy:args=[id] [--yes]
set -euo pipefail
export GIT_TERMINAL_PROMPT=0
export GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -oBatchMode=yes}"
PLUGINS_DIR="$HOME/.config/omarchy/plugins"
ASSUME_YES=0
UPDATED_ANY=0
fail() {
echo "omarchy-plugin-update: $*" >&2
exit 1
}
interactive() {
[[ -t 0 && -t 1 ]]
}
confirm() {
local prompt="$1"
(( ASSUME_YES )) && return 0
if interactive; then
gum confirm "$prompt"
else
fail "refusing to continue without confirmation; pass --yes"
fi
}
valid_plugin_id() {
[[ $1 =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ && $1 != *..* ]]
}
update_one() {
local id="$1"
local dir="$PLUGINS_DIR/$id"
if ! git -C "$dir" fetch --quiet origin HEAD; then
echo "omarchy-plugin-update: fetch failed for '$id'" >&2
return 1
fi
if [[ $(git -C "$dir" rev-parse HEAD) == $(git -C "$dir" rev-parse FETCH_HEAD) ]]; then
echo "$id is up to date."
return 0
fi
if (( ! ASSUME_YES )); then
echo "Changes for $id:"
if omarchy-cmd-present delta; then
git -C "$dir" diff HEAD FETCH_HEAD | delta --paging=never
else
git -C "$dir" diff HEAD FETCH_HEAD
fi
echo
confirm "Update $id?" || {
echo "Skipped $id."
return 0
}
fi
if ! git -C "$dir" merge --ff-only FETCH_HEAD >/dev/null 2>&1; then
echo "omarchy-plugin-update: cannot fast-forward '$id'; you have local changes in $dir" >&2
return 1
fi
if ! omarchy-plugin-validate "$dir"; then
git -C "$dir" reset --hard ORIG_HEAD >/dev/null
echo "omarchy-plugin-update: update of '$id' failed validation; rolled back" >&2
return 1
fi
echo "Updated $id."
UPDATED_ANY=1
}
id=""
while (( $# > 0 )); do
case "$1" in
--yes | -y)
ASSUME_YES=1
shift
;;
-h | --help)
echo "Usage: omarchy plugin update [id] [--yes]"
exit 0
;;
-*)
fail "unknown update option: $1"
;;
*)
[[ -z $id ]] || fail "unexpected argument: $1"
id="$1"
shift
;;
esac
done
[[ -d $PLUGINS_DIR ]] || fail "no plugins installed"
targets=()
if [[ -n $id ]]; then
valid_plugin_id "$id" || fail "invalid plugin id '$id'"
[[ -d $PLUGINS_DIR/$id ]] || fail "plugin '$id' is not installed"
[[ -d $PLUGINS_DIR/$id/.git ]] ||
fail "plugin '$id' is not a git checkout, so there is nothing to pull from"
targets=("$id")
else
for dir in "$PLUGINS_DIR"/*/; do
[[ -d $dir/.git ]] || continue
targets+=("$(basename "$dir")")
done
if (( ${#targets[@]} == 0 )); then
echo "No git-managed plugins installed."
exit 0
fi
fi
rc=0
for id in "${targets[@]}"; do
update_one "$id" || rc=1
done
if (( UPDATED_ANY )); then
omarchy-shell shell rescanPlugins >/dev/null
fi
exit $rc
+1 -13
View File
@@ -49,7 +49,7 @@ done
ID=$(jq -r '.id // ""' "$MANIFEST")
[[ -n $ID ]] || fail "manifest 'id' is empty"
[[ $ID =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || fail "invalid plugin id '$ID'"
[[ $ID != *"/"* && $ID != *".."* ]] || fail "invalid plugin id '$ID'"
[[ $ID != *".."* ]] || fail "invalid plugin id '$ID'"
[[ $ID != omarchy.* ]] || fail "plugin id '$ID' uses the reserved omarchy.* namespace"
# kinds must be a non-empty array.
@@ -115,16 +115,4 @@ done
link=$(find "$PLUGIN_DIR" -name .git -prune -o -type l -print -quit 2>/dev/null)
[[ -z $link ]] || fail "symlinks are not allowed inside a plugin folder: $link"
# The whole omarchy.* namespace plus every shipped first-party id is reserved.
# A third-party plugin claiming one of those ids would be rejected by the shell
# and could shadow built-in behaviour, so refuse it here too.
FIRST_PARTY_DIR="${OMARCHY_PATH:-$HOME/.local/share/omarchy}/shell/plugins"
if [[ -d $FIRST_PARTY_DIR ]]; then
while IFS= read -r fp_manifest; do
[[ -f $fp_manifest ]] || continue
fp_id=$(jq -r '.id // empty' "$fp_manifest" 2>/dev/null || true)
[[ $fp_id == "$ID" ]] && fail "plugin id '$ID' collides with a first-party Omarchy plugin"
done < <(find "$FIRST_PARTY_DIR" -type f \( -name manifest.json -o -name '*.manifest.json' \) 2>/dev/null)
fi
exit 0