#!/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 ] [--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: idbarWidgetPathnamecategoryallowMultiple 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 </ so you can edit it freely. The original stays built into Omarchy. Options: --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 --index --before --after 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 "$@"