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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
Executable
+170
@@ -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
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
|
||||
+146
-324
@@ -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
|
||||
|
||||
local source_id=""
|
||||
local new_id=""
|
||||
if (( $# > 0 )) && [[ $1 != --* ]]; then
|
||||
source_id=""
|
||||
if (( $# > 0 )) && [[ $1 != --* ]]; then
|
||||
source_id="$1"
|
||||
shift
|
||||
fi
|
||||
if (( $# > 0 )) && [[ $1 != --* ]]; then
|
||||
new_id="$1"
|
||||
shift
|
||||
fi
|
||||
fi
|
||||
|
||||
local display_name=""
|
||||
local replace="false"
|
||||
local add="false"
|
||||
local use_bar="false"
|
||||
local placement=()
|
||||
|
||||
while (( $# > 0 )); do
|
||||
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
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
fail "unknown clone option: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
done
|
||||
|
||||
if [[ -z $source_id ]]; then
|
||||
if interactive; then
|
||||
source_id=$(choose_clone_source) || fail "clone cancelled"
|
||||
else
|
||||
fail "clone source is required"
|
||||
fi
|
||||
fi
|
||||
[[ -n $source_id ]] || fail "clone source is required"
|
||||
|
||||
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"
|
||||
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"
|
||||
|
||||
local target_dir="$PLUGINS_DIR/$new_id"
|
||||
[[ ! -e $target_dir && ! -L $target_dir ]] || fail "$target_dir already exists"
|
||||
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 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"
|
||||
|
||||
Executable
+26
@@ -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"
|
||||
Executable
+43
@@ -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
|
||||
Executable
+46
@@ -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 }
|
||||
'
|
||||
Executable
+141
@@ -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
|
||||
Executable
+133
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -130,7 +130,7 @@ Run `omarchy --help` for the full list. The most common groups:
|
||||
| `omarchy toggle` | Toggle feature on/off | `omarchy toggle nightlight` |
|
||||
| `omarchy theme` | Theme management | `omarchy theme set <name>` |
|
||||
| `omarchy bar` | Bar layout and widgets | `omarchy bar plugin move omarchy.clock --section right` |
|
||||
| `omarchy plugin` | Manage/clone shell plugins | `omarchy plugin clone omarchy.clock local.clock --replace` |
|
||||
| `omarchy plugin` | Manage/clone shell plugins | `omarchy plugin clone omarchy.clock` |
|
||||
| `omarchy hook` | Install automation hooks | `omarchy hook install theme-set <script>` |
|
||||
| `omarchy install` | Install optional software / packages | `omarchy install docker dbs` |
|
||||
| `omarchy launch` | Launch apps | `omarchy launch browser` |
|
||||
@@ -183,9 +183,8 @@ To customize a built-in bar widget, never edit `$OMARCHY_PATH/shell/plugins/`.
|
||||
Clone it into the user plugin directory instead:
|
||||
|
||||
```bash
|
||||
omarchy plugin clone omarchy.workspaces local.workspaces --replace
|
||||
# Edit ~/.config/omarchy/plugins/local.workspaces/, then:
|
||||
omarchy plugin rescan
|
||||
omarchy plugin clone omarchy.workspaces
|
||||
# Edit ~/.config/omarchy/plugins/local.workspaces/; saved changes reload automatically.
|
||||
```
|
||||
|
||||
**Commands:** `omarchy restart shell`, `omarchy refresh shell`
|
||||
@@ -228,7 +227,7 @@ cp ~/.config/hypr/bindings.lua ~/.config/hypr/bindings.lua.bak.$(date +%s)
|
||||
|
||||
# 4. Apply changes
|
||||
# - Hyprland: auto-reloads on save, but MUST validate with `hyprctl reload` and `hyprctl configerrors`
|
||||
# - Omarchy shell: shell.json hot-reloads; use `omarchy plugin rescan` for plugin/widget code changes
|
||||
# - Omarchy shell: shell.json hot-reloads; use `omarchy-shell shell rescanPlugins` for plugin/widget code changes
|
||||
# - Launcher: restart with `omarchy restart shell`
|
||||
# - Terminals: MUST restart with `omarchy restart terminal`
|
||||
```
|
||||
@@ -420,6 +419,6 @@ This skill intentionally does not cover Omarchy source development. Do not use t
|
||||
- "Clear all reminders" -> `omarchy reminder clear`
|
||||
- "Customize the catppuccin theme colors" -> Create `~/.config/omarchy/themes/catppuccin-custom/` by copying from stock, then edit
|
||||
- "Run a script every time I change themes" -> Install it with `omarchy hook install theme-set <script>`
|
||||
- "Change how workspace labels are rendered" -> Clone `omarchy.workspaces` to a user plugin with `--replace`, then edit the clone
|
||||
- "Change how workspace labels are rendered" -> Clone `omarchy.workspaces`, which switches the bar to `local.workspaces`, then edit the clone
|
||||
- "Lock after ten minutes" -> Set `idle.lock` to `600` in `~/.config/omarchy/shell.json`
|
||||
- "Reset shell/bar to defaults" -> `omarchy refresh shell`
|
||||
|
||||
@@ -157,7 +157,8 @@
|
||||
"setup.plugin": {"icon":"","label":"Plugins","aliases":["plugin","plugins"]},
|
||||
"setup.plugin.enable": {"icon":"","label":"Enable Plugin","action":"omarchy-menu-plugin enable"},
|
||||
"setup.plugin.disable": {"icon":"","label":"Disable Plugin","action":"omarchy-menu-plugin disable"},
|
||||
"setup.plugin.add": {"icon":"","label":"Add Plugin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-plugin add'"},
|
||||
"setup.plugin.add": {"icon":"","label":"Add Plugin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-plugin-add'"},
|
||||
"setup.plugin.clone": {"icon":"","label":"Clone Plugin","action":"omarchy-menu-plugin clone"},
|
||||
// Only a plugin you installed yourself can be deleted, so Remove stays
|
||||
// hidden until there is one.
|
||||
"setup.plugin.remove": {"icon":"","label":"Remove Plugin","when":"compgen -G \"$HOME/.config/omarchy/plugins/*/manifest.json\"","action":"omarchy-menu-plugin remove"},
|
||||
|
||||
+13
-6
@@ -48,14 +48,21 @@ fast-forward pull:
|
||||
|
||||
```bash
|
||||
omarchy plugin add https://github.com/acme/omarchy-weather.git
|
||||
omarchy plugin update --all # fetches, shows a diff, fast-forwards
|
||||
omarchy plugin update # fetches, shows a diff, fast-forwards
|
||||
omarchy plugin remove acme.weather
|
||||
```
|
||||
|
||||
**Setup › Plugins** offers Enable, Disable, Add, and Remove. Enable and Disable
|
||||
include built-ins as well as installed plugins. Remove is limited to installed
|
||||
plugins, since a built-in has no checkout to delete. Add and Remove open a
|
||||
terminal so their warning, confirmation, and output stay visible.
|
||||
**Setup › Plugins** offers Enable, Disable, Add, Clone, and Remove. Enable and
|
||||
Disable include built-ins as well as installed plugins. Clone is limited to
|
||||
built-ins, while Remove is limited to installed plugins since a built-in has
|
||||
no checkout to delete. Add, Clone, and Remove open a terminal so their warning,
|
||||
editor, confirmation, and output stay visible.
|
||||
|
||||
Cloning `omarchy.clock`, for example, creates and switches to
|
||||
`~/.config/omarchy/plugins/local.clock/`, names it `My Clock`, and preserves
|
||||
the built-in IPC identity so existing shortcuts keep working. Saving files in
|
||||
a `local.*` clone reloads its code automatically, and removing an active clone
|
||||
switches back to its built-in source.
|
||||
|
||||
For a bar widget, on and off means its place in the bar. Everything else is
|
||||
loaded by default when it is built in, so `shell.json` records only the
|
||||
@@ -72,7 +79,7 @@ Commands prompt when run bare in a terminal and run unattended when given
|
||||
arguments — add `--yes` to skip every prompt (the path for scripts and agents).
|
||||
|
||||
You can still install by hand: drop a plugin into
|
||||
`~/.config/omarchy/plugins/<id>/`, run `omarchy plugin rescan`, then
|
||||
`~/.config/omarchy/plugins/<id>/`, run `omarchy-shell shell rescanPlugins`, then
|
||||
`omarchy plugin enable <id>`. A bar widget starts in its declared default
|
||||
section and can be moved with `omarchy bar plugin move`; enabling a full bar
|
||||
replaces the one in use.
|
||||
|
||||
+18
-10
@@ -100,7 +100,7 @@ manifest id); updating is a fast-forward pull of that checkout.
|
||||
```bash
|
||||
omarchy plugin add https://github.com/acme/omarchy-weather.git
|
||||
omarchy plugin update acme.weather # fetches, shows a diff, fast-forwards
|
||||
omarchy plugin update --all
|
||||
omarchy plugin update # updates every git-managed plugin
|
||||
omarchy plugin remove acme.weather
|
||||
```
|
||||
|
||||
@@ -116,7 +116,7 @@ AI agents:
|
||||
|
||||
```bash
|
||||
omarchy plugin add https://github.com/acme/omarchy-weather.git --enable --yes
|
||||
omarchy plugin update --all --yes
|
||||
omarchy plugin update --yes
|
||||
```
|
||||
|
||||
The installer never runs plugin code, install hooks, or sudo — it only clones
|
||||
@@ -130,26 +130,34 @@ You can still drop a plugin in without git:
|
||||
|
||||
1. Put it in `~/.config/omarchy/plugins/<plugin-id>/` with a `manifest.json`
|
||||
plus the QML referenced from its `entryPoints`.
|
||||
2. `omarchy plugin rescan`.
|
||||
2. `omarchy-shell shell rescanPlugins`.
|
||||
3. `omarchy plugin enable <id>`. Bar widgets start in
|
||||
`barWidget.defaultSection`, or in the center when it is omitted, and can be
|
||||
moved with `omarchy bar plugin move`; a full bar replaces the one in use.
|
||||
|
||||
The lower-level IPC equivalents remain available via `omarchy-shell shell rescanPlugins`,
|
||||
`omarchy-shell shell setPluginEnabled <id> true`, and `omarchy-shell shell listPlugins`.
|
||||
The `omarchy plugin` command wraps those calls and can also edit the persisted
|
||||
The `omarchy plugin` commands wrap those calls and can also edit the persisted
|
||||
bar layout in `shell.json`.
|
||||
|
||||
To hack on an existing widget safely, clone it into a user plugin instead of
|
||||
editing the built-in source. Third-party ids must be namespaced and may not use
|
||||
the reserved `omarchy.*` prefix.
|
||||
To hack on a built-in plugin safely, clone it into user config instead of
|
||||
editing the built-in source. The complete plugin directory is copied, including
|
||||
every declared kind and local dependency. A built-in id such as
|
||||
`omarchy.clock` becomes `local.clock`, with `My Clock` as its display name.
|
||||
|
||||
```bash
|
||||
omarchy plugin clone omarchy.clock local.clock --replace
|
||||
omarchy plugin clone # interactive source/name picker
|
||||
omarchy plugin edit local.clock # cd into the plugin directory
|
||||
omarchy plugin clone omarchy.clock
|
||||
```
|
||||
|
||||
Cloning switches from the built-in to the new local plugin, preserving an
|
||||
existing bar widget's position and settings. Setup > Plugins > Clone provides
|
||||
the interactive picker, then opens the new `local.*` directory in `$EDITOR`.
|
||||
Existing shortcuts and shell IPC calls made to the built-in id are routed to
|
||||
the enabled clone, so cloning does not require changing its callers. Removing
|
||||
an active clone switches back to its built-in source.
|
||||
Saving a file anywhere inside a `local.*` plugin reloads plugin code
|
||||
automatically; `omarchy-shell shell rescanPlugins` remains available to force a reload.
|
||||
|
||||
First-party plugins under `shell/plugins/` are discovered the same way and load
|
||||
by default. Disabling a non-widget records it in `disabledPlugins[]`; disabling
|
||||
a widget removes it from the bar layout while leaving its component available
|
||||
|
||||
@@ -180,4 +180,4 @@ Third-party widgets ship as separate plugins under
|
||||
declaring `kinds: ["bar-widget"]` and a `barWidget` entry point. See
|
||||
[../../README.md](../../README.md) for the manifest schema. Enable,
|
||||
rescan, and place third-party plugins with `omarchy plugin enable`,
|
||||
`omarchy plugin rescan`, and `omarchy bar plugin add`.
|
||||
`omarchy-shell shell rescanPlugins`, and `omarchy bar plugin add`.
|
||||
|
||||
@@ -66,5 +66,13 @@
|
||||
"defaultValue": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"omarchy": {
|
||||
"clonePaths": [
|
||||
{
|
||||
"source": "../indicators",
|
||||
"target": "indicators"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,5 +16,13 @@
|
||||
"description": "Status notifier items",
|
||||
"category": "Status",
|
||||
"allowMultiple": false
|
||||
},
|
||||
"omarchy": {
|
||||
"clonePaths": [
|
||||
{
|
||||
"source": "TrayModel.js",
|
||||
"target": "TrayModel.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ QtObject {
|
||||
signal pluginsChanged()
|
||||
signal scanFinished()
|
||||
signal pluginLoadFailed(string id, string error)
|
||||
signal localPluginChanged(string id)
|
||||
|
||||
// ---------------------------------------------------------------- helpers
|
||||
|
||||
@@ -141,6 +142,19 @@ QtObject {
|
||||
&& config.disabledPlugins.indexOf(Util.canonicalWidgetId(String(id))) !== -1
|
||||
}
|
||||
|
||||
function resolveEnabledId(id) {
|
||||
var key = Util.canonicalWidgetId(String(id || ""))
|
||||
// Callers keep using the built-in id after cloning; the enabled local
|
||||
// manifest is the implementation that should receive the call.
|
||||
for (var candidate in installedPlugins) {
|
||||
var manifest = installedPlugins[candidate]
|
||||
var metadata = manifest && Util.isPlainObject(manifest.omarchy) ? manifest.omarchy : null
|
||||
if (metadata && String(metadata.clonedFrom || "") === key && isEnabled(candidate))
|
||||
return candidate
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// A bar widget is on when it sits in the bar, whoever shipped it. That is a
|
||||
// different question from isEnabled(), which decides whether the widget's
|
||||
// component is loaded at all — a built-in stays loadable so it can be put
|
||||
@@ -200,6 +214,8 @@ QtObject {
|
||||
}
|
||||
var isBarOption = manifest && Array.isArray(manifest.kinds) && manifest.kinds.indexOf("bar") !== -1
|
||||
var isBarWidget = manifest && Array.isArray(manifest.kinds) && manifest.kinds.indexOf("bar-widget") !== -1
|
||||
var hasNonWidgetKind = manifest && Array.isArray(manifest.kinds)
|
||||
&& manifest.kinds.some(function(kind) { return kind !== "bar-widget" })
|
||||
shellConfigMutator(function(config) {
|
||||
// Ensure shape exists.
|
||||
if (!Util.isPlainObject(config.bar)) config.bar = { layout: { left: [], center: [], right: [] } }
|
||||
@@ -241,7 +257,7 @@ QtObject {
|
||||
else if (location.kind === "plugin") config.plugins.splice(location.index, 1)
|
||||
// Dropping the layout entry is the whole story for a widget. Anything
|
||||
// else built-in loads by default, so switching it off has to be stated.
|
||||
if (isFirstParty && !isBarWidget && !isDisabled(config, key)) {
|
||||
if (isFirstParty && (!isBarWidget || hasNonWidgetKind) && !isDisabled(config, key)) {
|
||||
if (!Array.isArray(config.disabledPlugins)) config.disabledPlugins = []
|
||||
config.disabledPlugins.push(key)
|
||||
}
|
||||
@@ -337,7 +353,36 @@ QtObject {
|
||||
}
|
||||
|
||||
property Process initProcess: Process {
|
||||
onExited: registry.rescan()
|
||||
onExited: {
|
||||
localPluginWatcher.running = true
|
||||
registry.rescan()
|
||||
}
|
||||
}
|
||||
|
||||
property Process localPluginWatcher: Process {
|
||||
command: [
|
||||
"inotifywait",
|
||||
"-m",
|
||||
"-r",
|
||||
"-q",
|
||||
"-e",
|
||||
"close_write,create,delete,move",
|
||||
"--format",
|
||||
"%w%f",
|
||||
registry.pluginsDir
|
||||
]
|
||||
stdout: SplitParser {
|
||||
onRead: function(path) {
|
||||
var pluginId = registry.localPluginIdForPath(path)
|
||||
if (pluginId) registry.localPluginChanged(pluginId)
|
||||
}
|
||||
}
|
||||
onExited: localPluginWatcherRestart.restart()
|
||||
}
|
||||
|
||||
property Timer localPluginWatcherRestart: Timer {
|
||||
interval: 1000
|
||||
onTriggered: localPluginWatcher.running = true
|
||||
}
|
||||
|
||||
function rescan() {
|
||||
@@ -379,5 +424,17 @@ QtObject {
|
||||
initProcess.running = true
|
||||
}
|
||||
|
||||
function localPluginIdForPath(filePath) {
|
||||
var base = pluginsDir.replace(/\/$/, "") + "/"
|
||||
var path = String(filePath || "").trim()
|
||||
if (path.indexOf(base + "local.") !== 0) return ""
|
||||
|
||||
var relative = path.slice(base.length)
|
||||
if (relative.indexOf("/.git/") !== -1 || relative.endsWith("/.git")) return ""
|
||||
|
||||
var slash = relative.indexOf("/")
|
||||
return slash === -1 ? relative : relative.slice(0, slash)
|
||||
}
|
||||
|
||||
Component.onCompleted: ensureUserDir()
|
||||
}
|
||||
|
||||
+17
-6
@@ -58,6 +58,12 @@ ShellRoot {
|
||||
property bool pluginReloading: false
|
||||
property bool pluginReloadPending: false
|
||||
|
||||
Timer {
|
||||
id: localPluginReloadTimer
|
||||
interval: 150
|
||||
onTriggered: shell.reloadPlugins()
|
||||
}
|
||||
|
||||
onShellConfigChanged: {
|
||||
if (failedBarId !== "") failedBarId = ""
|
||||
pluginRegistry.registryRevision++
|
||||
@@ -440,7 +446,7 @@ ShellRoot {
|
||||
}
|
||||
|
||||
function summon(pluginId, payloadJson) {
|
||||
var id = String(pluginId || "")
|
||||
var id = shell.pluginRegistry.resolveEnabledId(pluginId)
|
||||
if (!id) return false
|
||||
var plugins = shell.pluginRegistry.installedPlugins
|
||||
if (!plugins[id]) {
|
||||
@@ -480,7 +486,7 @@ ShellRoot {
|
||||
}
|
||||
|
||||
function hide(pluginId) {
|
||||
var id = String(pluginId || "")
|
||||
var id = shell.pluginRegistry.resolveEnabledId(pluginId)
|
||||
if (!id) return false
|
||||
if (shell.isBarWidgetPanelPlugin(id)) {
|
||||
var hidden = shell.bar && typeof shell.bar.hideBarWidget === "function"
|
||||
@@ -497,7 +503,7 @@ ShellRoot {
|
||||
}
|
||||
|
||||
function isPluginOpen(pluginId) {
|
||||
var id = String(pluginId || "")
|
||||
var id = shell.pluginRegistry.resolveEnabledId(pluginId)
|
||||
if (shell.isBarWidgetPanelPlugin(id)) {
|
||||
return shell.bar && typeof shell.bar.isBarWidgetOpen === "function"
|
||||
? shell.bar.isBarWidgetOpen(id)
|
||||
@@ -510,7 +516,7 @@ ShellRoot {
|
||||
}
|
||||
|
||||
function toggle(pluginId, payloadJson) {
|
||||
var id = String(pluginId || "")
|
||||
var id = shell.pluginRegistry.resolveEnabledId(pluginId)
|
||||
return isPluginOpen(id) ? hide(id) : summon(id, payloadJson)
|
||||
}
|
||||
|
||||
@@ -567,14 +573,15 @@ ShellRoot {
|
||||
}
|
||||
|
||||
function callIfLoaded(pluginId, method, arg) {
|
||||
var loader = panelLoaders[pluginId]
|
||||
var id = shell.pluginRegistry.resolveEnabledId(pluginId)
|
||||
var loader = panelLoaders[id]
|
||||
if (!loader || !loader.item) return "unknown"
|
||||
if (typeof loader.item[method] !== "function") return "unknown"
|
||||
try {
|
||||
var result = loader.item[method](arg)
|
||||
return result === undefined || result === null ? "ok" : String(result)
|
||||
} catch (e) {
|
||||
console.warn("plugin " + pluginId + " " + method + "() threw:", e)
|
||||
console.warn("plugin " + id + " " + method + "() threw:", e)
|
||||
return "error"
|
||||
}
|
||||
}
|
||||
@@ -761,6 +768,10 @@ ShellRoot {
|
||||
|
||||
Connections {
|
||||
target: shell.pluginRegistry
|
||||
function onLocalPluginChanged(pluginId) {
|
||||
console.log("Local plugin changed, reloading:", pluginId)
|
||||
localPluginReloadTimer.restart()
|
||||
}
|
||||
function onScanFinished() {
|
||||
if (shell.pluginReloadPending) {
|
||||
shell.pluginReloadPending = false
|
||||
|
||||
@@ -82,8 +82,12 @@ ShellRoot {
|
||||
scan += block("firstparty", "/first/widgets/clock", manifest("omarchy.first-widget", ["bar-widget"], { barWidget: "Widget.qml" }))
|
||||
scan += block("firstparty", "/first/bar", manifest("omarchy.bar", ["bar"], { bar: "Bar.qml" }))
|
||||
scan += block("firstparty", "/first/panels/grouped", manifest("omarchy.grouped-panel", ["panel"], { panel: "Panel.qml" }))
|
||||
scan += block("firstparty", "/first/hybrid", manifest("omarchy.hybrid", ["menu", "bar-widget"], { menu: "Menu.qml", barWidget: "Widget.qml" }))
|
||||
scan += block("thirdparty", "/third/panel", manifest("third.panel", ["panel"], { panel: "Panel.qml" }))
|
||||
scan += block("thirdparty", "/third/widget", manifest("third.widget", ["bar-widget"], { barWidget: "Widget.qml" }, { defaultSection: "left" }))
|
||||
var localWidget = manifest("local.first-widget", ["bar-widget"], { barWidget: "Widget.qml" })
|
||||
localWidget.omarchy = { clonedFrom: "omarchy.first-widget" }
|
||||
scan += block("thirdparty", "/third/local-widget", localWidget)
|
||||
scan += block("thirdparty", "/third/bar", manifest("third.bar", ["bar"], { bar: "Bar.qml" }))
|
||||
scan += block("thirdparty", "/third/shadow", manifest("omarchy.first-widget", ["panel"], { panel: "Panel.qml" }))
|
||||
scan += block("thirdparty", "/third/reserved", manifest("omarchy.reserved", ["panel"], { panel: "Panel.qml" }))
|
||||
@@ -96,9 +100,11 @@ ShellRoot {
|
||||
registry.parseScanOutput(scan)
|
||||
|
||||
root.assertDeepEqual(pluginIds(), [
|
||||
"local.first-widget",
|
||||
"omarchy.bar",
|
||||
"omarchy.first-widget",
|
||||
"omarchy.grouped-panel",
|
||||
"omarchy.hybrid",
|
||||
"third.bar",
|
||||
"third.panel",
|
||||
"third.widget"
|
||||
@@ -120,6 +126,7 @@ ShellRoot {
|
||||
root.assertTrue(registry.isEnabled("omarchy.bar"), "built-in bar option is active by default")
|
||||
root.assertTrue(!registry.isEnabled("third.bar"), "third-party bar options start inactive")
|
||||
root.assertTrue(!registry.isEnabled("third.panel"), "third-party plugins start disabled")
|
||||
root.assertEqual(registry.resolveEnabledId("omarchy.first-widget"), "omarchy.first-widget", "inactive clones do not replace their source id")
|
||||
|
||||
registry.setEnabled("third.bar", true)
|
||||
root.assertEqual(root.config.bar.id, "third.bar", "enabling third-party bar options writes bar id")
|
||||
@@ -141,6 +148,10 @@ ShellRoot {
|
||||
registry.setEnabled("third.widget", false)
|
||||
root.assertDeepEqual(root.config.bar.layout.left, [], "disabling bar widgets removes layout entry")
|
||||
|
||||
registry.setEnabled("local.first-widget", true)
|
||||
root.assertEqual(registry.resolveEnabledId("omarchy.first-widget"), "local.first-widget", "enabled clones receive calls made to their source id")
|
||||
registry.setEnabled("local.first-widget", false)
|
||||
|
||||
root.config = {
|
||||
version: 1,
|
||||
bar: { layout: { left: [], center: [{ id: "third.widget", size: 4 }], right: [] } },
|
||||
@@ -183,6 +194,21 @@ ShellRoot {
|
||||
registry.setEnabled("omarchy.first-widget", true)
|
||||
root.assertDeepEqual(root.config.bar.layout.center, [{ id: "omarchy.first-widget" }], "a widget without a default section falls back to center")
|
||||
|
||||
root.config = {
|
||||
version: 1,
|
||||
bar: { layout: { left: [{ id: "omarchy.hybrid" }], center: [], right: [] } },
|
||||
plugins: []
|
||||
}
|
||||
registry.setEnabled("omarchy.hybrid", false)
|
||||
root.assertDeepEqual(root.config.bar.layout.left, [], "disabling a multi-kind built-in removes its widget")
|
||||
root.assertDeepEqual(root.config.disabledPlugins, ["omarchy.hybrid"], "disabling a multi-kind built-in unloads its other kinds")
|
||||
root.assertTrue(!registry.isEnabled("omarchy.hybrid"), "a disabled multi-kind built-in is not loadable")
|
||||
|
||||
var localBase = registry.pluginsDir + "/local.clock"
|
||||
root.assertEqual(registry.localPluginIdForPath(localBase + "/BarWidget.qml"), "local.clock", "local clone changes are watched")
|
||||
root.assertEqual(registry.localPluginIdForPath(registry.pluginsDir + "/acme.clock/BarWidget.qml"), "", "installed plugins are not treated as local clones")
|
||||
root.assertEqual(registry.localPluginIdForPath(localBase + "/.git/index"), "", "clone git metadata is ignored")
|
||||
|
||||
root.assertTrue(changeCount > 0, "registry emits change notifications")
|
||||
writeResult()
|
||||
}
|
||||
|
||||
@@ -12,15 +12,21 @@ trap 'rm -rf "$TMPDIR"' EXIT
|
||||
STUB_DIR="$TMPDIR/stub"
|
||||
mkdir -p "$STUB_DIR"
|
||||
|
||||
# The picker reads the plugin list from omarchy-plugin and hands what it decided
|
||||
# back to it, so stubbing both ends shows which plugin a pick actually resolved
|
||||
# to -- the thing a source-level check cannot see.
|
||||
cat >"$STUB_DIR/omarchy-plugin" <<'STUB'
|
||||
# The picker reads the plugin list from omarchy-plugin-list and hands what it
|
||||
# decided to a verb-specific command, so stubbing both ends shows which plugin
|
||||
# a pick actually resolved to -- the thing a source-level check cannot see.
|
||||
cat >"$STUB_DIR/omarchy-plugin-list" <<'STUB'
|
||||
#!/bin/bash
|
||||
[[ $1 == list ]] && { cat "$FAKE_PLUGINS"; exit 0; }
|
||||
printf 'omarchy-plugin %s\n' "$*" >>"$FAKE_CALLS"
|
||||
cat "$FAKE_PLUGINS"
|
||||
STUB
|
||||
|
||||
for command in omarchy-plugin-enable omarchy-plugin-disable; do
|
||||
cat >"$STUB_DIR/$command" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf '%s %s\n' "${0##*/}" "$*" >>"$FAKE_CALLS"
|
||||
STUB
|
||||
done
|
||||
|
||||
# Records the rows it was offered, then answers with the pick under test.
|
||||
cat >"$STUB_DIR/omarchy-menu-select" <<'STUB'
|
||||
#!/bin/bash
|
||||
@@ -47,6 +53,7 @@ pick() {
|
||||
|
||||
: >"$TMPDIR/calls"
|
||||
: >"$TMPDIR/rows"
|
||||
HOME="$TMPDIR/home" \
|
||||
PATH="$STUB_DIR:$PATH" \
|
||||
FAKE_PLUGINS="$TMPDIR/plugins.json" \
|
||||
FAKE_CALLS="$TMPDIR/calls" \
|
||||
@@ -58,9 +65,8 @@ pick() {
|
||||
CALLS=$(cat "$TMPDIR/calls")
|
||||
}
|
||||
|
||||
# Cloning a plugin keeps the name it was cloned from, so two plugins really can
|
||||
# arrive at the picker calling themselves Clock. Both eligible for the same
|
||||
# verb: neither row can stand on the name alone.
|
||||
# Two plugins can declare the same display name. When both are eligible for the
|
||||
# same verb, neither row can stand on the name alone.
|
||||
cat >"$TMPDIR/plugins.json" <<'JSON'
|
||||
[
|
||||
{"id": "omarchy.clock", "name": "Clock", "kinds": ["bar-widget"], "enabled": false, "active": false, "canDisable": true, "firstParty": true},
|
||||
@@ -72,7 +78,7 @@ pick enable "Clock (local.clock)"
|
||||
[[ $ROWS == *"Clock (omarchy.clock)"* && $ROWS == *"Clock (local.clock)"* ]] \
|
||||
|| fail "picker tells two plugins of the same name apart" "$ROWS"
|
||||
pass "picker tells two plugins of the same name apart"
|
||||
[[ $CALLS == *"omarchy-plugin enable local.clock"* ]] \
|
||||
[[ $CALLS == *"omarchy-plugin-enable local.clock"* ]] \
|
||||
|| fail "picker acts on the row that was picked, not the one that shares its name" "$CALLS"
|
||||
pass "picker acts on the row that was picked, not the one that shares its name"
|
||||
|
||||
@@ -88,12 +94,12 @@ JSON
|
||||
pick enable "Clock"
|
||||
[[ $ROWS != *"("* ]] || fail "picker adorns a row only when its name is taken twice over" "$ROWS"
|
||||
pass "picker adorns a row only when its name is taken twice over"
|
||||
[[ $CALLS == *"omarchy-plugin enable local.clock"* ]] \
|
||||
[[ $CALLS == *"omarchy-plugin-enable local.clock"* ]] \
|
||||
|| fail "picker resolves a lone row to the plugin the verb offered, not a namesake it filtered out" "$CALLS"
|
||||
pass "picker resolves a lone row to the plugin the verb offered, not a namesake it filtered out"
|
||||
|
||||
pick remove "Clock"
|
||||
[[ $CALLS == *"omarchy-plugin remove local.clock"* ]] \
|
||||
[[ $CALLS == *"omarchy-plugin-remove local.clock"* ]] \
|
||||
|| fail "picker removes the plugin whose row was picked" "$CALLS"
|
||||
pass "picker removes the plugin whose row was picked"
|
||||
|
||||
@@ -108,10 +114,40 @@ pick enable "Weather"
|
||||
[[ $ROWS == *"Weather"* && $ROWS != *"acme.weather)"* ]] \
|
||||
|| fail "picker leaves an unambiguous name unadorned" "$ROWS"
|
||||
pass "picker leaves an unambiguous name unadorned"
|
||||
[[ $CALLS == *"omarchy-plugin enable acme.weather"* ]] \
|
||||
[[ $CALLS == *"omarchy-plugin-enable acme.weather"* ]] \
|
||||
|| fail "picker delegates plugin enablement to the plugin command" "$CALLS"
|
||||
pass "picker delegates plugin enablement to the plugin command"
|
||||
|
||||
# Clone offers only first-party plugins without an existing local counterpart,
|
||||
# then performs the clone and opens its deterministic path in $EDITOR.
|
||||
cat >"$TMPDIR/plugins.json" <<'JSON'
|
||||
[
|
||||
{"id": "omarchy.clock", "name": "Clock", "kinds": ["bar-widget"], "enabled": true, "active": false, "canDisable": true, "firstParty": true},
|
||||
{"id": "acme.weather", "name": "Weather", "kinds": ["bar-widget"], "enabled": false, "active": false, "canDisable": true, "firstParty": false}
|
||||
]
|
||||
JSON
|
||||
|
||||
pick clone "Clock"
|
||||
[[ $ROWS == *"Clock"* && $ROWS != *"Weather"* ]] ||
|
||||
fail "clone picker offers only built-in plugins" "$ROWS"
|
||||
pass "clone picker offers built-in plugins"
|
||||
[[ $CALLS == *'terminal: omarchy-plugin-clone omarchy.clock && exec $EDITOR '*"/.config/omarchy/plugins/local.clock" ]] ||
|
||||
fail "clone picker opens the cloned path in EDITOR" "$CALLS"
|
||||
pass "clone picker clones and opens the local plugin"
|
||||
|
||||
# Once local.<id> is discovered, the source no longer belongs in Clone.
|
||||
cat >"$TMPDIR/plugins.json" <<'JSON'
|
||||
[
|
||||
{"id": "omarchy.clock", "name": "Clock", "kinds": ["bar-widget"], "enabled": true, "active": false, "canDisable": true, "firstParty": true},
|
||||
{"id": "local.clock", "name": "My Clock", "kinds": ["bar-widget"], "enabled": false, "active": false, "canDisable": true, "firstParty": false}
|
||||
]
|
||||
JSON
|
||||
|
||||
pick clone ""
|
||||
[[ $CALLS == *"notification: No plugin to clone"* ]] ||
|
||||
fail "clone picker offers an already cloned plugin" "$CALLS"
|
||||
pass "clone picker omits plugins already cloned locally"
|
||||
|
||||
# The picker treats every plugin alike and leaves kind-specific behavior to the
|
||||
# plugin command.
|
||||
cat >"$TMPDIR/plugins.json" <<'JSON'
|
||||
@@ -121,7 +157,7 @@ cat >"$TMPDIR/plugins.json" <<'JSON'
|
||||
JSON
|
||||
|
||||
pick enable "Fancy"
|
||||
[[ $CALLS == *"omarchy-plugin enable acme.fancy"* && $CALLS != *"--section"* ]] \
|
||||
[[ $CALLS == *"omarchy-plugin-enable acme.fancy"* && $CALLS != *"--section"* ]] \
|
||||
|| fail "picker delegates kind-specific enablement" "$CALLS"
|
||||
pass "picker delegates kind-specific enablement"
|
||||
|
||||
@@ -151,7 +187,7 @@ pick enable "Bar"
|
||||
[[ $ROWS == *"Bar"* && $ROWS != *"Neon Bar"* ]] \
|
||||
|| fail "picker offers every bar except the one already running" "$ROWS"
|
||||
pass "picker offers every bar except the one already running"
|
||||
[[ $CALLS == *"omarchy-plugin enable omarchy.bar"* ]] \
|
||||
[[ $CALLS == *"omarchy-plugin-enable omarchy.bar"* ]] \
|
||||
|| fail "picker returns to the built-in bar by enabling it" "$CALLS"
|
||||
pass "picker returns to the built-in bar by enabling it"
|
||||
|
||||
|
||||
+12
-10
@@ -183,11 +183,11 @@ assertEqual(
|
||||
)
|
||||
assertDeepEqual(
|
||||
defaultItems.filter(item => item.parent === 'setup.plugin').map(item => item.label),
|
||||
['Enable Plugin', 'Disable Plugin', 'Add Plugin', 'Remove Plugin'],
|
||||
['Enable Plugin', 'Disable Plugin', 'Add Plugin', 'Clone Plugin', 'Remove Plugin'],
|
||||
'menu manages plugins from Setup > Plugins'
|
||||
)
|
||||
assert(
|
||||
['enable', 'disable', 'remove'].every(
|
||||
['enable', 'disable', 'clone', 'remove'].every(
|
||||
verb => defaultById[`setup.plugin.${verb}`].action === `omarchy-menu-plugin ${verb}`
|
||||
),
|
||||
'menu picks a plugin the way it already picks a theme or a timezone'
|
||||
@@ -201,7 +201,7 @@ assert(
|
||||
'menu hides Remove until a plugin the user installed exists to delete'
|
||||
)
|
||||
assert(
|
||||
defaultById['setup.plugin.add'].action.includes('omarchy-plugin add'),
|
||||
defaultById['setup.plugin.add'].action.includes('omarchy-plugin-add'),
|
||||
'menu adds a plugin through the CLI, where the trust warning and clone output are visible'
|
||||
)
|
||||
|
||||
@@ -212,23 +212,25 @@ assert(
|
||||
)
|
||||
assert(
|
||||
/remove\).*\(\.firstParty \| not\)/.test(pluginPicker)
|
||||
&& /clone\).*\.firstParty/.test(pluginPicker)
|
||||
&& !/kinds|bar-widget|A_BAR_OPTION|NOT_A_BAR_OPTION|BAR_ICON/.test(pluginPicker),
|
||||
'plugin picker leaves plugin-kind decisions to its data and the plugin command'
|
||||
)
|
||||
|
||||
const pluginCli = fs.readFileSync(path.join(root, 'bin/omarchy-plugin'), 'utf8')
|
||||
const pluginAdd = fs.readFileSync(path.join(root, 'bin/omarchy-plugin-add'), 'utf8')
|
||||
const pluginEnable = fs.readFileSync(path.join(root, 'bin/omarchy-plugin-enable'), 'utf8')
|
||||
assert(
|
||||
/Now using \$id as the bar/.test(pluginCli)
|
||||
&& /enabled_message "\$id"[\s\S]*?place_bar_widget/.test(pluginCli),
|
||||
/Now using \$id as the bar/.test(pluginEnable)
|
||||
&& /Now using \$id as the bar[\s\S]*?place_bar_widget/.test(pluginAdd),
|
||||
'plugin enable reports a bar as replacing the one in use, whether enabled or freshly added'
|
||||
)
|
||||
assert(
|
||||
/\.barWidget\.defaultSection \/\/ "center"/.test(pluginCli)
|
||||
&& /gum choose[\s\S]*?--selected "\$default_section"/.test(pluginCli),
|
||||
/\.barWidget\.defaultSection \/\/ "center"/.test(pluginAdd)
|
||||
&& /gum choose[\s\S]*?--selected "\$default_section"/.test(pluginAdd),
|
||||
'interactive plugin add selects the manifest placement or center fallback by default'
|
||||
)
|
||||
assert(
|
||||
/omarchy-plugin "\$1" "\$id"/.test(pluginPicker),
|
||||
/"omarchy-plugin-\$1" "\$id"/.test(pluginPicker),
|
||||
'plugin picker delegates enable and disable without interpreting plugin kinds'
|
||||
)
|
||||
// Icons ride along as "<glyph>\tlabel"; the menu shows the glyph and hands
|
||||
@@ -245,7 +247,7 @@ assert(
|
||||
'menu select mode reads a leading icon off an option and filters on the label alone'
|
||||
)
|
||||
assert(
|
||||
/omarchy-launch-floating-terminal-with-presentation "omarchy-plugin remove/.test(pluginPicker),
|
||||
/omarchy-launch-floating-terminal-with-presentation "omarchy-plugin-remove/.test(pluginPicker),
|
||||
'plugin picker removes where the confirmation and backup path are visible'
|
||||
)
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ git -C "$incoming" add .
|
||||
git -C "$incoming" -c user.name=Test -c user.email=test@example.com commit -qm "Initial"
|
||||
|
||||
output=$(HOME="$test_home" OMARCHY_PATH="$ROOT" PATH="$stub_dir:$ROOT/bin:$PATH" \
|
||||
omarchy-plugin add "$incoming" --yes 2>&1) &&
|
||||
omarchy-plugin-add "$incoming" --yes 2>&1) &&
|
||||
fail "plugin add accepts an id already installed under another directory" "$output"
|
||||
grep -qF "plugin id 'acme.same' is already used by" <<<"$output" ||
|
||||
fail "plugin add explains the installed id collision" "$output"
|
||||
|
||||
@@ -4,45 +4,216 @@ set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
require_command jq
|
||||
require_command python3
|
||||
|
||||
TMPDIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TMPDIR"' EXIT
|
||||
mkdir -p "$TMPDIR/home/.config/omarchy"
|
||||
mkdir -p "$TMPDIR/home/.config/omarchy" "$TMPDIR/bin"
|
||||
CALLS="$TMPDIR/calls"
|
||||
|
||||
clone() {
|
||||
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" PATH="$ROOT/bin:$PATH" \
|
||||
omarchy-plugin-clone "$1" "$2" --name "$3"
|
||||
cat >"$TMPDIR/bin/omarchy-shell" <<'SH'
|
||||
#!/bin/bash
|
||||
if [[ $* == *"listShellConfig"* ]]; then
|
||||
if [[ -n ${FAKE_SHELL_CONFIG:-} ]]; then
|
||||
printf '%s\n' "$FAKE_SHELL_CONFIG"
|
||||
else
|
||||
printf '{}\n'
|
||||
fi
|
||||
elif [[ $* == *"listPlugins"* ]]; then
|
||||
if [[ ${FAKE_NO_DISCOVERY:-0} == 1 ]]; then
|
||||
printf '[]\n'
|
||||
else
|
||||
find "$HOME/.config/omarchy/plugins" -mindepth 2 -maxdepth 2 -name manifest.json -print0 |
|
||||
xargs -0 -r jq -s 'map({id: .id, enabled: true})'
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
SH
|
||||
|
||||
for command in omarchy-bar omarchy-bar-plugin; do
|
||||
cat >"$TMPDIR/bin/$command" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s %s\n' "${0##*/}" "$*" >>"$FAKE_CALLS"
|
||||
exec "$OMARCHY_TEST_ROOT/bin/${0##*/}" "$@"
|
||||
SH
|
||||
done
|
||||
|
||||
for command in omarchy-plugin-enable omarchy-plugin-disable omarchy-notification-send; do
|
||||
cat >"$TMPDIR/bin/$command" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s %s\n' "${0##*/}" "$*" >>"$FAKE_CALLS"
|
||||
SH
|
||||
done
|
||||
chmod +x "$TMPDIR/bin/"*
|
||||
|
||||
clone_plugin() {
|
||||
local default_config='{
|
||||
"bar": {
|
||||
"layout": {
|
||||
"left": [{"id": "omarchy.menu"}],
|
||||
"center": [{"id": "omarchy.clock", "format": "HH:mm"}],
|
||||
"right": []
|
||||
}
|
||||
}
|
||||
}'
|
||||
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" PATH="$TMPDIR/bin:$ROOT/bin:$PATH" \
|
||||
FAKE_CALLS="$CALLS" OMARCHY_TEST_ROOT="$ROOT" \
|
||||
FAKE_SHELL_CONFIG="${FAKE_CLONE_CONFIG:-$default_config}" \
|
||||
omarchy-plugin-clone "$@"
|
||||
}
|
||||
|
||||
# A bar widget that pulls in a sibling JS module clones into a directory that
|
||||
# does not contain it, so the import has to be rewritten back to the bundled
|
||||
# file or the cloned widget fails to load.
|
||||
clone omarchy.clock local.clock-clone "Cloned Clock" >/dev/null
|
||||
widget="$TMPDIR/home/.config/omarchy/plugins/local.clock-clone/Widget.qml"
|
||||
clone_plugin omarchy.clock >/dev/null
|
||||
clock="$TMPDIR/home/.config/omarchy/plugins/local.clock"
|
||||
|
||||
[[ -f $widget ]] || fail "clone produces a widget file"
|
||||
pass "clone produces a widget file"
|
||||
|
||||
grep -q 'moduleName: "local.clock-clone"' "$widget" ||
|
||||
fail "clone rewrites the module name"
|
||||
pass "clone rewrites the module name"
|
||||
|
||||
while read -r ref; do
|
||||
[[ $ref == file://* ]] || fail "clone leaves a relative reference behind" "$ref"
|
||||
done < <(grep -oE '(import|Qt\.resolvedUrl\() *"[^"]+"' "$widget" |
|
||||
grep -oE '"[^"]+"' | tr -d '"' | grep -E '\.(js|qml)$')
|
||||
pass "clone resolves every relative QML and JS reference to the bundled file"
|
||||
|
||||
for ref in $(grep -oE 'file://[^"]+\.(js|qml)' "$widget"); do
|
||||
path=${ref#file://}
|
||||
[[ -f $path ]] || fail "cloned reference points at a real file" "$ref"
|
||||
for file in manifest.json BarWidget.qml Panel.qml Model.js; do
|
||||
[[ -f $clock/$file ]] || fail "clock clone is missing $file"
|
||||
done
|
||||
pass "cloned references point at files that exist"
|
||||
pass "clone copies the complete plugin"
|
||||
|
||||
# The bundled widget genuinely has such an import, so the check above is not
|
||||
# passing by accident.
|
||||
grep -qE '^import "[^"/][^"]*\.js"' "$ROOT/shell/plugins/panels/clock/BarWidget.qml" ||
|
||||
fail "the clock widget still imports a sibling JS module"
|
||||
pass "the clock widget still imports a sibling JS module"
|
||||
grep -q 'import "Model.js"' "$clock/BarWidget.qml" &&
|
||||
grep -q 'Qt.resolvedUrl("Panel.qml")' "$clock/BarWidget.qml" ||
|
||||
fail "clock clone does not preserve local dependencies"
|
||||
pass "clone keeps plugin dependencies local"
|
||||
|
||||
rg -qF "omarchy.clock" "$clock" -g '*.qml' -g '*.js' ||
|
||||
fail "clock clone does not preserve the stable runtime id"
|
||||
pass "clone preserves the built-in runtime IPC id"
|
||||
|
||||
jq -e '
|
||||
.id == "local.clock" and
|
||||
.name == "My Clock" and
|
||||
.barWidget.displayName == "My Clock" and
|
||||
.omarchy.clonedFrom == "omarchy.clock" and
|
||||
.kinds == ["bar-widget"] and
|
||||
.entryPoints.barWidget == "BarWidget.qml"
|
||||
' "$clock/manifest.json" >/dev/null || fail "clock clone manifest is incorrect"
|
||||
pass "clone updates identity without replacing the manifest"
|
||||
|
||||
grep -qx 'omarchy-bar-plugin replace omarchy.clock local.clock' "$CALLS" ||
|
||||
fail "clone does not replace an active bar widget"
|
||||
grep -qx 'omarchy-notification-send -g Editing Cloned Plugin Original plugin has been replace by clone.' "$CALLS" ||
|
||||
fail "clone does not notify that the editable clone is active"
|
||||
jq -e '
|
||||
any(.bar.layout.center[];
|
||||
.id == "local.clock" and
|
||||
.format == "dddd HH:mm" and
|
||||
.formatAlt == "d MMMM \u0027W\u0027ww yyyy")
|
||||
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null ||
|
||||
fail "clone does not preserve the active widget's placement and settings"
|
||||
pass "clone switches bar widgets in place and confirms the editable clone"
|
||||
|
||||
jq '.bar.layout.center += ["omarchy.keyboard-layout"]' \
|
||||
"$TMPDIR/home/.config/omarchy/shell.json" >"$TMPDIR/shell.json"
|
||||
mv "$TMPDIR/shell.json" "$TMPDIR/home/.config/omarchy/shell.json"
|
||||
FAKE_CLONE_CONFIG='{
|
||||
"bar": {
|
||||
"layout": {
|
||||
"left": [],
|
||||
"center": ["omarchy.keyboard-layout"],
|
||||
"right": []
|
||||
}
|
||||
}
|
||||
}' clone_plugin omarchy.keyboard-layout >/dev/null
|
||||
jq -e '
|
||||
any(.bar.layout.center[]; .id == "local.keyboard-layout")
|
||||
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null ||
|
||||
fail "clone does not replace a string-form bar entry"
|
||||
pass "clone replaces legacy string-form bar entries"
|
||||
|
||||
clone_plugin omarchy.menu >/dev/null
|
||||
menu="$TMPDIR/home/.config/omarchy/plugins/local.menu"
|
||||
|
||||
for file in manifest.json Menu.qml MenuModel.js BarWidget.qml; do
|
||||
[[ -f $menu/$file ]] || fail "menu clone is missing $file"
|
||||
done
|
||||
jq -e '
|
||||
.id == "local.menu" and
|
||||
.kinds == ["menu", "bar-widget"] and
|
||||
.entryPoints.menu == "Menu.qml" and
|
||||
.entryPoints.barWidget == "BarWidget.qml"
|
||||
' "$menu/manifest.json" >/dev/null || fail "menu clone loses plugin kinds"
|
||||
grep -qx 'omarchy-plugin-disable omarchy.menu' "$CALLS" ||
|
||||
fail "clone leaves the built-in half of a multi-kind plugin enabled"
|
||||
pass "clone preserves multi-kind plugins"
|
||||
|
||||
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" PATH="$TMPDIR/bin:$ROOT/bin:$PATH" \
|
||||
FAKE_CALLS="$CALLS" OMARCHY_TEST_ROOT="$ROOT" \
|
||||
omarchy-plugin-remove local.menu --yes >/dev/null
|
||||
grep -qx 'omarchy-bar-plugin replace local.menu omarchy.menu' "$CALLS" &&
|
||||
grep -qx 'omarchy-plugin-enable omarchy.menu' "$CALLS" ||
|
||||
fail "removing a clone does not restore its built-in source"
|
||||
pass "removing a clone restores its built-in source"
|
||||
|
||||
clone_plugin omarchy.active-window >/dev/null
|
||||
[[ -f $TMPDIR/home/.config/omarchy/plugins/local.active-window/ActiveWindow.qml ]] ||
|
||||
fail "flat bar plugin clone is incomplete"
|
||||
pass "flat bar plugins clone from adjacent manifests"
|
||||
|
||||
grep -qx 'omarchy-bar-plugin add local.active-window' "$CALLS" ||
|
||||
fail "clone does not activate a bar widget whose source is absent"
|
||||
jq -e '
|
||||
any(.bar.layout.left[]; .id == "local.active-window")
|
||||
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null ||
|
||||
fail "clone does not add the absent widget using its default placement"
|
||||
pass "clone activates an absent bar widget"
|
||||
|
||||
clone_plugin omarchy.indicators >/dev/null
|
||||
indicators="$TMPDIR/home/.config/omarchy/plugins/local.indicators"
|
||||
for file in Indicators.qml indicators/Dnd.qml indicators/Reminder.qml; do
|
||||
[[ -f $indicators/$file ]] || fail "indicators clone is missing $file"
|
||||
done
|
||||
grep -q 'Qt.resolvedUrl("indicators/"' "$indicators/Indicators.qml" ||
|
||||
fail "indicators clone does not point at its copied components"
|
||||
pass "flat bar plugins declare extra clone dependencies"
|
||||
|
||||
clone_plugin omarchy.tray >/dev/null
|
||||
[[ -f $TMPDIR/home/.config/omarchy/plugins/local.tray/TrayModel.js ]] ||
|
||||
fail "tray clone is missing its model"
|
||||
pass "flat bar plugins keep local script dependencies"
|
||||
|
||||
clone_plugin omarchy.bar >/dev/null
|
||||
grep -qx 'omarchy-bar use local.bar' "$CALLS" ||
|
||||
fail "clone does not select a cloned bar"
|
||||
jq -e '.bar.id == "local.bar"' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null ||
|
||||
fail "clone does not persist the cloned bar as active"
|
||||
pass "clone switches full bars"
|
||||
|
||||
clone_plugin omarchy.background >/dev/null
|
||||
grep -qx 'omarchy-plugin-enable local.background' "$CALLS" ||
|
||||
fail "clone does not enable an ordinary cloned plugin"
|
||||
grep -qx 'omarchy-plugin-disable omarchy.background' "$CALLS" ||
|
||||
fail "clone does not disable the ordinary source plugin"
|
||||
pass "clone switches ordinary plugins"
|
||||
|
||||
mkdir -p "$TMPDIR/home/.config/omarchy/plugins/acme.example"
|
||||
cat >"$TMPDIR/home/.config/omarchy/plugins/acme.example/manifest.json" <<'JSON'
|
||||
{"id":"acme.example","name":"Example","kinds":["bar-widget"],"entryPoints":{"barWidget":"Widget.qml"}}
|
||||
JSON
|
||||
if clone_plugin acme.example >/dev/null 2>&1; then
|
||||
fail "clone accepts a user plugin"
|
||||
fi
|
||||
pass "clone is limited to built-in plugins"
|
||||
|
||||
if clone_plugin omarchy.weather custom.weather >/dev/null 2>&1; then
|
||||
fail "clone accepts a custom id"
|
||||
fi
|
||||
[[ ! -e $TMPDIR/home/.config/omarchy/plugins/local.weather ]] ||
|
||||
fail "rejected custom id leaves a clone behind"
|
||||
pass "clone derives the local id"
|
||||
|
||||
if clone_plugin omarchy.weather --replace >/dev/null 2>&1; then
|
||||
fail "clone still accepts bar layout actions"
|
||||
fi
|
||||
[[ ! -e $TMPDIR/home/.config/omarchy/plugins/local.weather ]] ||
|
||||
fail "rejected bar action leaves a clone behind"
|
||||
pass "clone does not accept manual switch options"
|
||||
|
||||
if clone_plugin >/dev/null 2>&1; then
|
||||
fail "clone opens an interactive picker without a source id"
|
||||
fi
|
||||
pass "clone requires an explicit source id"
|
||||
|
||||
if FAKE_NO_DISCOVERY=1 clone_plugin omarchy.osd >/dev/null 2>&1; then
|
||||
fail "clone succeeds before the shell discovers it"
|
||||
fi
|
||||
[[ ! -e $TMPDIR/home/.config/omarchy/plugins/local.osd ]] ||
|
||||
fail "failed clone discovery leaves a partial clone behind"
|
||||
pass "clone removes a partial clone when switching fails"
|
||||
|
||||
@@ -156,6 +156,33 @@ for (const manifestPath of manifests) {
|
||||
if (relativePath.endsWith('.manifest.json')) {
|
||||
check(JSON.stringify(manifest.kinds) === JSON.stringify(['bar-widget']), `${manifest.id} sibling manifest must be a bar widget`)
|
||||
}
|
||||
|
||||
const clonePaths = manifest.omarchy?.clonePaths
|
||||
if (clonePaths !== undefined) {
|
||||
check(Array.isArray(clonePaths), `${manifest.id} omarchy.clonePaths must be an array`)
|
||||
const cloneTargets = new Set()
|
||||
for (const clonePath of Array.isArray(clonePaths) ? clonePaths : []) {
|
||||
const valid = isPlainObject(clonePath)
|
||||
&& typeof clonePath.source === 'string'
|
||||
&& /^[A-Za-z0-9_./-]+$/.test(clonePath.source)
|
||||
&& typeof clonePath.target === 'string'
|
||||
&& /^[A-Za-z0-9_./-]+$/.test(clonePath.target)
|
||||
&& !clonePath.target.startsWith('/')
|
||||
&& !clonePath.target.includes('..')
|
||||
check(
|
||||
valid,
|
||||
`${manifest.id} clone paths must have safe source and target paths`
|
||||
)
|
||||
if (valid) {
|
||||
check(
|
||||
fs.existsSync(path.resolve(path.dirname(manifestPath), clonePath.source)),
|
||||
`${manifest.id} clone source ${clonePath.source} must exist`
|
||||
)
|
||||
check(!cloneTargets.has(clonePath.target), `${manifest.id} clone target ${clonePath.target} must be unique`)
|
||||
cloneTargets.add(clonePath.target)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const byId = Object.fromEntries(manifests.map(manifestPath => {
|
||||
|
||||
@@ -53,6 +53,28 @@ cp -a "$ROOT/shell" "$test_root/shell"
|
||||
ln -s "$ROOT/config" "$test_root/config"
|
||||
ln -s "$ROOT/bin" "$test_root/bin"
|
||||
|
||||
hot_reload_dir="$test_home/.config/omarchy/plugins/local.hot-reload"
|
||||
mkdir -p "$hot_reload_dir"
|
||||
cat >"$hot_reload_dir/manifest.json" <<'JSON'
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "local.hot-reload",
|
||||
"name": "Before Hot Reload",
|
||||
"version": "1.0.0",
|
||||
"kinds": ["overlay"],
|
||||
"entryPoints": {"overlay": "Overlay.qml"},
|
||||
"omarchy": {"clonedFrom": "omarchy.emojis"}
|
||||
}
|
||||
JSON
|
||||
cat >"$hot_reload_dir/Overlay.qml" <<'QML'
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
function open(payloadJson) {}
|
||||
function close() {}
|
||||
}
|
||||
QML
|
||||
|
||||
cat >"$stub_bin/omarchy-update-available" <<'SH'
|
||||
#!/bin/bash
|
||||
echo "Omarchy update available (test)"
|
||||
@@ -116,6 +138,31 @@ jq -e '
|
||||
}
|
||||
pass "shell IPC lists plugin metadata"
|
||||
|
||||
jq '.name = "After Hot Reload"' "$hot_reload_dir/manifest.json" >"$hot_reload_dir/manifest.json.tmp"
|
||||
mv "$hot_reload_dir/manifest.json.tmp" "$hot_reload_dir/manifest.json"
|
||||
|
||||
hot_reload_name=""
|
||||
for _ in {1..80}; do
|
||||
hot_reload_name=$(shell_ipc shell listPlugins 2>/dev/null |
|
||||
jq -r '.[] | select(.id == "local.hot-reload") | .name' 2>/dev/null || true)
|
||||
[[ $hot_reload_name == "After Hot Reload" ]] && break
|
||||
if ! kill -0 "$QS_PID" 2>/dev/null; then
|
||||
fail_with_log "test shell exited while reloading a changed local clone"
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
[[ $hot_reload_name == "After Hot Reload" ]] ||
|
||||
fail_with_log "local clone changes reload without an explicit rescan"
|
||||
pass "local clone changes reload without an explicit rescan"
|
||||
|
||||
[[ $(shell_ipc shell setPluginEnabled local.hot-reload true) == "ok" ]] ||
|
||||
fail_with_log "local clone could not be enabled"
|
||||
[[ $(shell_ipc shell summon omarchy.emojis "{}") == "ok" ]] ||
|
||||
fail_with_log "calls to a cloned source id do not reach its enabled clone"
|
||||
shell_ipc_quiet shell hide omarchy.emojis >/dev/null
|
||||
shell_ipc_quiet shell setPluginEnabled local.hot-reload false >/dev/null
|
||||
pass "shell IPC routes built-in ids to enabled clones"
|
||||
|
||||
shell_config=$(shell_ipc shell listShellConfig)
|
||||
jq -e '
|
||||
.version == 1 and
|
||||
|
||||
Reference in New Issue
Block a user