Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
509 lines
14 KiB
Bash
Executable File
509 lines
14 KiB
Bash
Executable File
#!/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
|
|
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
|
|
}
|
|
|
|
require_command() {
|
|
omarchy-cmd-present "$1" || fail "$1 is required"
|
|
}
|
|
|
|
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 != *..* ]]
|
|
}
|
|
|
|
installed_plugin_ids() {
|
|
find "$PLUGINS_DIR" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) ! -name '.*' -printf '%f\n' 2>/dev/null | sort
|
|
}
|
|
|
|
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
|
|
|
|
require_command jq
|
|
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
|
|
|
|
omarchy-shell shell rescanPlugins >/dev/null 2>&1 || true
|
|
if [[ $enabled == "true" ]]; then
|
|
require_command jq
|
|
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
|
|
echo "Enabled $id"
|
|
else
|
|
echo "Disabled $id"
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------- add
|
|
|
|
plugin_add() {
|
|
require_command git
|
|
require_command jq
|
|
|
|
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
|
|
id=$(jq -r '.id' "$stage/manifest.json")
|
|
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
|
|
echo "Enabled $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() {
|
|
require_command git
|
|
|
|
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
|