Files
omarchycn/bin/omarchy-plugin-clone
T

363 lines
11 KiB
Bash
Executable File

#!/bin/bash
# omarchy:summary=Clone a built-in or user 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
set -euo pipefail
PLUGINS_DIR="$HOME/.config/omarchy/plugins"
fail() {
echo "omarchy-plugin-clone: $*" >&2
exit 1
}
require_command() {
omarchy-cmd-present "$1" || fail "$1 is required"
}
require_omarchy_path() {
[[ -n ${OMARCHY_PATH:-} ]] || fail "OMARCHY_PATH is not set"
}
interactive() {
[[ -t 0 && -t 1 ]]
}
slug_id() {
tr '[:upper:]' '[:lower:]' <<<"$1" | sed -E 's/^omarchy\.//; s/[^a-z0-9]+/-/g; s/^-+//; s/-+$//'
}
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
require_command jq
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)
# 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
}
usage() {
cat <<USAGE
Usage: omarchy plugin clone [source-id] [new-id] [options]
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.
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_command jq
require_command python3
require_omarchy_path
local source_id=""
local new_id=""
if (( $# > 0 )) && [[ $1 != --* ]]; then
source_id="$1"
shift
fi
if (( $# > 0 )) && [[ $1 != --* ]]; then
new_id="$1"
shift
fi
local display_name=""
local replace="false"
local add="false"
local use_bar="false"
local placement=()
while (( $# > 0 )); do
case "$1" in
--name)
display_name="${2:-}"
[[ -n $display_name ]] || fail "--name requires a value"
shift 2
;;
--replace)
replace="true"
shift
;;
--add)
add="true"
shift
;;
--use)
use_bar="true"
shift
;;
--section | --index | --before | --after)
placement+=("$1" "${2:-}")
shift 2
;;
-h | --help)
usage
return
;;
*)
fail "unknown clone option: $1"
;;
esac
done
if [[ -z $source_id ]]; then
if interactive; then
source_id=$(choose_clone_source) || fail "clone cancelled"
else
fail "clone source is required"
fi
fi
local default_slug
default_slug=$(slug_id "$source_id")
if [[ -z $new_id ]]; then
if interactive; then
new_id=$(input_value "New plugin id:" "local.$default_slug")
else
fail "new plugin id is required"
fi
fi
validate_plugin_id "$new_id"
local target_dir="$PLUGINS_DIR/$new_id"
[[ ! -e $target_dir && ! -L $target_dir ]] || fail "$target_dir already exists"
local source_path=""
local source_name=""
local source_category="Plugin"
local source_multiple="false"
local source_type=""
if IFS=$'\t' read -r _ source_path source_name source_category source_multiple < <(builtin_widget_info "$source_id"); then
source_type="builtin-widget"
elif [[ -f $PLUGINS_DIR/$source_id/manifest.json ]]; then
source_type="user-plugin"
source_path="$PLUGINS_DIR/$source_id"
source_name=$(jq -r '.name // .id' "$source_path/manifest.json")
else
# Non-widget built-in plugin (e.g. a panel or bar): find it in the catalog.
local hit
hit=$(catalog_json | jq -r --arg id "$source_id" '.[] | select(.id == $id) | .sourceDir' | head -1)
if [[ -n $hit ]]; then
source_type="builtin-plugin"
source_path="$hit"
source_name=$(jq -r '.name // .id' "$source_path/manifest.json")
fi
fi
[[ -n $source_type ]] || fail "unknown clone source: $source_id"
if [[ -z $display_name ]]; then
if interactive; then
display_name=$(input_value "Display name:" "${source_name:-$new_id}")
else
display_name="${source_name:-$new_id}"
fi
fi
[[ -n $display_name ]] || fail "display name is required"
mkdir -p "$target_dir"
if [[ $source_type == "builtin-widget" ]]; then
cp "$source_path" "$target_dir/Widget.qml"
rewrite_cloned_qml "$target_dir/Widget.qml" "$new_id" "$source_path"
write_clone_manifest "$target_dir/manifest.json" "$new_id" "$display_name" "Cloned from $source_id" "$source_category" "$source_multiple" "$source_id" "$source_path"
else
cp -aL "$source_path/." "$target_dir/"
update_cloned_manifest "$target_dir/manifest.json" "$new_id" "$display_name" "$source_id" "$source_path"
fi
{
printf '# %s\n\n' "$display_name"
printf 'Cloned from `%s`.\n\n' "$source_id"
printf 'Source: `%s`\n\n' "$source_path"
printf 'The original remains built into Omarchy. Remove this plugin directory or swap\n'
printf 'your bar entry back to `%s` whenever you want to return to the built-in.\n' "$source_id"
} >"$target_dir/UPSTREAM.md"
omarchy-shell -q shell rescanPlugins >/dev/null 2>&1 || true
if [[ $replace == "true" ]]; then
omarchy-bar-plugin replace "$source_id" "$new_id"
echo "Cloned $source_id to $new_id and replaced the first bar instance"
elif [[ $add == "true" ]]; then
omarchy-bar-plugin add "$new_id" "${placement[@]}"
echo "Cloned $source_id to $new_id"
elif [[ $use_bar == "true" ]]; then
omarchy-bar use "$new_id"
echo "Cloned $source_id to $new_id and selected it as the active bar option"
else
echo "Cloned $source_id to $new_id"
fi
if interactive; then
cd "$target_dir"
exec "${SHELL:-bash}"
fi
}
clone_command "$@"