Last active
May 10, 2026 18:25
-
-
Save dnicolson/de1b8a7d0ee1bb31b09eb7f8afd38a65 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # | |
| # Add an app bundle to the Finder toolbar without replacing the stock items. | |
| # | |
| # Usage: | |
| # ./add-to-finder-toolbar.sh /Applications/SomeApp.app POSITION | |
| # POSITION is 0-based; use -1 for the last position. | |
| set -euo pipefail | |
| PREF_DOMAIN="com.apple.finder" | |
| if [[ $# -ne 2 ]]; then | |
| echo "Usage: $0 /path/to/App.app POSITION" >&2 | |
| echo "POSITION is 0-based; use -1 for the last position." >&2 | |
| exit 1 | |
| fi | |
| TARGET="$1" | |
| POSITION="$2" | |
| if [[ ! -d "$TARGET" ]]; then | |
| echo "Error: '$TARGET' is not an app bundle." >&2 | |
| exit 1 | |
| fi | |
| if ! [[ "$POSITION" =~ ^-?[0-9]+$ ]]; then | |
| echo "Error: POSITION must be an integer." >&2 | |
| exit 1 | |
| fi | |
| TARGET="$(cd "$(dirname "$TARGET")" && pwd)/$(basename "$TARGET")" | |
| tmpdir="$(mktemp -d)" | |
| trap 'rm -rf "$tmpdir"' EXIT | |
| prefs_in="$tmpdir/finder-in.plist" | |
| prefs_out="$tmpdir/finder-out.plist" | |
| defaults export "$PREF_DOMAIN" - > "$prefs_in" | |
| python3 - "$TARGET" "$POSITION" "$prefs_in" "$prefs_out" <<'PY' | |
| import copy | |
| import plistlib | |
| import re | |
| import subprocess | |
| import sys | |
| from pathlib import Path | |
| APP_PATH = sys.argv[1] | |
| POSITION = int(sys.argv[2]) | |
| PREFS_IN = Path(sys.argv[3]) | |
| PREFS_OUT = Path(sys.argv[4]) | |
| IDENTIFIER = "com.apple.finder.loc " | |
| APPKIT_KEY = "NSToolbar Configuration Browser" | |
| FINDER_KEY = "FXInfoPanesExpanded" | |
| FINDER_BINARY = "/System/Library/CoreServices/Finder.app/Contents/MacOS/Finder" | |
| def bail(message): | |
| print(f"ERROR: {message}", file=sys.stderr) | |
| sys.exit(1) | |
| def info(message): | |
| print(message, file=sys.stderr) | |
| def cloned_dict(value): | |
| return copy.deepcopy(value) if isinstance(value, dict) else {} | |
| def normalized_identifiers(value): | |
| if isinstance(value, list): | |
| return list(value) | |
| if isinstance(value, dict): | |
| def sort_key(item_key): | |
| try: | |
| return int(item_key) | |
| except (TypeError, ValueError): | |
| return item_key | |
| return [value[key] for key in sorted(value, key=sort_key)] | |
| return [] | |
| def effective_identifiers(config): | |
| identifiers = normalized_identifiers(config.get("TB Item Identifiers")) | |
| if identifiers: | |
| return identifiers | |
| defaults = normalized_identifiers(config.get("TB Default Item Identifiers")) | |
| if defaults: | |
| return defaults | |
| return [] | |
| def identifier_from_kind_token(token): | |
| special_tokens = { | |
| "spac": "NSToolbarSpaceItem", | |
| "sSpc": "NSToolbarSpaceItem", | |
| "sepa": "NSToolbarSeparatorItemIdentifier", | |
| "flxs": "NSToolbarFlexibleSpaceItemIdentifier", | |
| "CMTB": "NSToolbarCustomizeToolbarItemIdentifier", | |
| } | |
| if token in special_tokens: | |
| return special_tokens[token] | |
| if re.fullmatch(r"[A-Z]{4}", token): | |
| return f"com.apple.finder.{token}" | |
| return None | |
| def decode_kind_runs(blob): | |
| runs = [] | |
| for offset in range(4): | |
| run = [] | |
| for index in range(offset, len(blob) - 3, 4): | |
| token = blob[index:index + 4][::-1] | |
| identifier = identifier_from_kind_token(token) | |
| if identifier is None: | |
| if run: | |
| runs.append(run) | |
| run = [] | |
| continue | |
| run.append(identifier) | |
| if run: | |
| runs.append(run) | |
| return runs | |
| def finder_binary_default_identifiers(): | |
| try: | |
| result = subprocess.run( | |
| ["otool", "-s", "__TEXT", "__const", FINDER_BINARY], | |
| capture_output=True, | |
| check=True, | |
| text=True, | |
| ) | |
| except (OSError, subprocess.CalledProcessError): | |
| return [] | |
| cstring_bytes = bytearray() | |
| for line in result.stdout.splitlines(): | |
| parts = line.strip().split() | |
| if len(parts) < 2 or not re.fullmatch(r"[0-9A-Fa-f]{16}", parts[0]): | |
| continue | |
| for word in parts[1:]: | |
| if re.fullmatch(r"[0-9A-Fa-f]{8}", word): | |
| cstring_bytes.extend(bytes.fromhex(word)[::-1]) | |
| exact_match_count = 10 | |
| decoded = cstring_bytes.decode("latin-1", errors="ignore") | |
| for candidate in re.findall(r"[A-Za-z]{20,}", decoded): | |
| for run in decode_kind_runs(candidate): | |
| if len(run) >= exact_match_count: | |
| return run[:exact_match_count] | |
| return [] | |
| with PREFS_IN.open("rb") as handle: | |
| prefs = plistlib.load(handle) | |
| finder_config = prefs.get(FINDER_KEY) | |
| appkit_config = prefs.get(APPKIT_KEY) | |
| if isinstance(appkit_config, dict): | |
| config = cloned_dict(appkit_config) | |
| info(f"Found '{APPKIT_KEY}' — using as base.") | |
| if isinstance(finder_config, dict): | |
| for key, value in finder_config.items(): | |
| config[key] = copy.deepcopy(value) | |
| info(f"Overlaying custom items from '{FINDER_KEY}'.") | |
| elif isinstance(finder_config, dict): | |
| config = cloned_dict(finder_config) | |
| info(f"'{APPKIT_KEY}' absent — using '{FINDER_KEY}' as base.") | |
| else: | |
| bail( | |
| "Neither 'FXInfoPanesExpanded' nor 'NSToolbar Configuration Browser' " | |
| "found in com.apple.finder." | |
| ) | |
| identifiers = effective_identifiers(config) | |
| raw_plists = config.get("TB Item Plists", {}) | |
| item_plists = dict(raw_plists) if raw_plists else {} | |
| if not identifiers: | |
| identifiers = finder_binary_default_identifiers() | |
| if not identifiers: | |
| bail( | |
| "Finder does not currently expose toolbar identifiers in prefs, " | |
| "and the default toolbar could not be derived from Finder.app." | |
| ) | |
| info("Derived the default toolbar from Finder.app.") | |
| info(f"Current toolbar ({len(identifiers)} items): {identifiers}") | |
| file_url = "file://" + APP_PATH.rstrip("/") + "/" | |
| existing_index = None | |
| for index, identifier in enumerate(identifiers): | |
| if identifier != IDENTIFIER: | |
| continue | |
| entry = item_plists.get(str(index)) | |
| if isinstance(entry, dict) and entry.get("_CFURLString") == file_url: | |
| existing_index = index | |
| break | |
| if IDENTIFIER in identifiers: | |
| duplicate_index = identifiers.index(IDENTIFIER) | |
| identifiers.pop(duplicate_index) | |
| new_plists = {} | |
| for key, value in item_plists.items(): | |
| try: | |
| numeric_key = int(key) | |
| except (TypeError, ValueError): | |
| new_plists[key] = value | |
| continue | |
| if numeric_key < duplicate_index: | |
| new_plists[str(numeric_key)] = value | |
| elif numeric_key > duplicate_index: | |
| new_plists[str(numeric_key - 1)] = value | |
| item_plists = new_plists | |
| if POSITION == -1: | |
| insert_index = len(identifiers) | |
| else: | |
| insert_index = max(0, min(POSITION, len(identifiers))) | |
| if existing_index is not None and existing_index == insert_index: | |
| info(f"'{APP_PATH}' is already present at position {insert_index}. Nothing to do.") | |
| sys.exit(0) | |
| identifiers.insert(insert_index, IDENTIFIER) | |
| new_plists = {} | |
| for key, value in item_plists.items(): | |
| try: | |
| numeric_key = int(key) | |
| except (TypeError, ValueError): | |
| new_plists[key] = value | |
| continue | |
| if numeric_key >= insert_index: | |
| new_plists[str(numeric_key + 1)] = value | |
| else: | |
| new_plists[str(numeric_key)] = value | |
| new_plists[str(insert_index)] = { | |
| "_CFURLString": file_url, | |
| "_CFURLStringType": 15, | |
| } | |
| item_plists = new_plists | |
| config["TB Item Identifiers"] = identifiers | |
| config["TB Item Plists"] = item_plists | |
| finder_out = cloned_dict(finder_config) | |
| appkit_out = cloned_dict(appkit_config) or cloned_dict(config) | |
| finder_out["TB Item Identifiers"] = copy.deepcopy(identifiers) | |
| finder_out["TB Item Plists"] = copy.deepcopy(item_plists) | |
| appkit_out.update(copy.deepcopy(config)) | |
| appkit_out["TB Item Identifiers"] = copy.deepcopy(identifiers) | |
| appkit_out["TB Item Plists"] = copy.deepcopy(item_plists) | |
| prefs[FINDER_KEY] = finder_out | |
| prefs[APPKIT_KEY] = appkit_out | |
| with PREFS_OUT.open("wb") as handle: | |
| plistlib.dump(prefs, handle, fmt=plistlib.FMT_XML) | |
| info(f"Written: index {insert_index} → '{IDENTIFIER}' → {file_url}") | |
| PY | |
| if [[ ! -s "$prefs_out" ]]; then | |
| exit 0 | |
| fi | |
| defaults import "$PREF_DOMAIN" "$prefs_out" | |
| echo "Restarting Finder..." | |
| killall Finder >/dev/null 2>&1 || true | |
| echo "Done." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment