Created
December 14, 2025 14:44
-
-
Save gnattu/34e6a52c2105501e0766c0246ab43537 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 | |
| set -euo pipefail | |
| IFS=$'\n\t' | |
| usage() { | |
| cat <<'EOF' | |
| extract-dmg-layout.sh — extract Finder layout settings from an existing DMG. | |
| This mounts the DMG, opens the disk in Finder, and queries: | |
| - window position/size | |
| - icon size / text size | |
| - background picture path (if set) | |
| - icon positions for *.app and the Applications link (if present) | |
| Usage: | |
| scripts/extract-dmg-layout.sh --dmg /path/Old.dmg [--export-dir dist/dmg-layout] | |
| Options: | |
| --dmg PATH Path to .dmg file (required) | |
| --export-dir DIR Where to copy the extracted background image (default: dist/dmg-layout) | |
| -h, --help Show help | |
| Output: | |
| Prints a ready-to-paste create-dmg snippet using the extracted values. | |
| EOF | |
| } | |
| DMG_PATH="" | |
| EXPORT_DIR="dist/dmg-layout" | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| --dmg) DMG_PATH="${2:-}"; shift 2 ;; | |
| --export-dir) EXPORT_DIR="${2:-}"; shift 2 ;; | |
| -h|--help) usage; exit 0 ;; | |
| *) | |
| echo "Unknown arg: $1" >&2 | |
| usage >&2 | |
| exit 2 ;; | |
| esac | |
| done | |
| if [[ -z "$DMG_PATH" ]]; then | |
| echo "Missing required --dmg" >&2 | |
| usage >&2 | |
| exit 2 | |
| fi | |
| if [[ ! -f "$DMG_PATH" || "${DMG_PATH##*.}" != "dmg" ]]; then | |
| echo "--dmg must point to a .dmg file: $DMG_PATH" >&2 | |
| exit 2 | |
| fi | |
| for tool in hdiutil osascript; do | |
| if ! command -v "$tool" >/dev/null 2>&1; then | |
| echo "Missing required tool: $tool" >&2 | |
| exit 127 | |
| fi | |
| done | |
| TMP_ROOT="$(mktemp -d)" | |
| cleanup() { | |
| set +e | |
| if [[ -n "${MOUNTPOINT:-}" ]]; then | |
| hdiutil detach "$MOUNTPOINT" >/dev/null 2>&1 || hdiutil detach -force "$MOUNTPOINT" >/dev/null 2>&1 | |
| fi | |
| rm -rf "$TMP_ROOT" | |
| } | |
| trap cleanup EXIT | |
| ATTACH_OUT="$(hdiutil attach -nobrowse -readonly -noverify "$DMG_PATH")" | |
| MOUNTPOINT="$(printf '%s\n' "$ATTACH_OUT" | awk '/\/Volumes\// {print $NF; exit}')" | |
| if [[ -z "$MOUNTPOINT" || ! -d "$MOUNTPOINT" ]]; then | |
| echo "Failed to find mountpoint for DMG." >&2 | |
| echo "hdiutil output:" >&2 | |
| printf '%s\n' "$ATTACH_OUT" >&2 | |
| exit 1 | |
| fi | |
| VOLNAME="$(basename "$MOUNTPOINT")" | |
| APP_PATH="$(find "$MOUNTPOINT" -maxdepth 1 -name '*.app' -print -quit || true)" | |
| APP_NAME="" | |
| if [[ -n "$APP_PATH" ]]; then | |
| APP_NAME="$(basename "$APP_PATH")" | |
| fi | |
| if [[ -z "$APP_NAME" ]]; then | |
| echo "No .app found at DMG root: $MOUNTPOINT" >&2 | |
| echo "Contents:" >&2 | |
| ls -la "$MOUNTPOINT" >&2 | |
| exit 1 | |
| fi | |
| RESULT="$( | |
| osascript <<EOF | |
| on _toText(v) | |
| try | |
| return v as text | |
| on error | |
| return "" | |
| end try | |
| end _toText | |
| on run | |
| set volName to "${VOLNAME}" | |
| set appName to "${APP_NAME}" | |
| tell application "Finder" | |
| activate | |
| tell disk volName | |
| open | |
| delay 0.5 | |
| set w to container window | |
| set b to bounds of w | |
| set leftX to item 1 of b | |
| set topY to item 2 of b | |
| set rightX to item 3 of b | |
| set bottomY to item 4 of b | |
| set winW to (rightX - leftX) | |
| set winH to (bottomY - topY) | |
| set vo to icon view options of w | |
| set iconSize to icon size of vo | |
| set textSize to text size of vo | |
| set bgPosix to "" | |
| try | |
| set bgAlias to background picture of vo | |
| set bgPosix to POSIX path of (bgAlias as alias) | |
| end try | |
| set appX to "" | |
| set appY to "" | |
| try | |
| set p to position of item appName of w | |
| set appX to item 1 of p | |
| set appY to item 2 of p | |
| end try | |
| set appsX to "" | |
| set appsY to "" | |
| try | |
| set p2 to position of item "Applications" of w | |
| set appsX to item 1 of p2 | |
| set appsY to item 2 of p2 | |
| end try | |
| close w | |
| set outText to "" | |
| set outText to outText & "win_x=" & my _toText(leftX) & linefeed | |
| set outText to outText & "win_y=" & my _toText(topY) & linefeed | |
| set outText to outText & "win_w=" & my _toText(winW) & linefeed | |
| set outText to outText & "win_h=" & my _toText(winH) & linefeed | |
| set outText to outText & "icon_size=" & my _toText(iconSize) & linefeed | |
| set outText to outText & "text_size=" & my _toText(textSize) & linefeed | |
| set outText to outText & "bg_path=" & my _toText(bgPosix) & linefeed | |
| set outText to outText & "app_x=" & my _toText(appX) & linefeed | |
| set outText to outText & "app_y=" & my _toText(appY) & linefeed | |
| set outText to outText & "apps_x=" & my _toText(appsX) & linefeed | |
| set outText to outText & "apps_y=" & my _toText(appsY) & linefeed | |
| return outText | |
| end tell | |
| end tell | |
| end run | |
| EOF | |
| )" | |
| WIN_X=""; WIN_Y=""; WIN_W=""; WIN_H=""; ICON_SIZE=""; TEXT_SIZE=""; BG_PATH=""; APP_X=""; APP_Y=""; APPS_X=""; APPS_Y="" | |
| while IFS='=' read -r k v; do | |
| [[ -z "${k:-}" ]] && continue | |
| case "$k" in | |
| win_x) WIN_X="$v" ;; | |
| win_y) WIN_Y="$v" ;; | |
| win_w) WIN_W="$v" ;; | |
| win_h) WIN_H="$v" ;; | |
| icon_size) ICON_SIZE="$v" ;; | |
| text_size) TEXT_SIZE="$v" ;; | |
| bg_path) BG_PATH="$v" ;; | |
| app_x) APP_X="$v" ;; | |
| app_y) APP_Y="$v" ;; | |
| apps_x) APPS_X="$v" ;; | |
| apps_y) APPS_Y="$v" ;; | |
| esac | |
| done <<<"$RESULT" | |
| sanitize_num() { | |
| # keep digits, optional leading '-', and optional '.'; strip commas/spaces/locale artifacts | |
| local in="${1:-}" | |
| in="${in//,/}" | |
| in="${in// /}" | |
| printf '%s' "$in" | tr -cd '0-9.-' | sed 's/^\.\+//' | |
| } | |
| WIN_X="$(sanitize_num "$WIN_X")" | |
| WIN_Y="$(sanitize_num "$WIN_Y")" | |
| WIN_W="$(sanitize_num "$WIN_W")" | |
| WIN_H="$(sanitize_num "$WIN_H")" | |
| ICON_SIZE="$(sanitize_num "$ICON_SIZE")" | |
| TEXT_SIZE="$(sanitize_num "$TEXT_SIZE")" | |
| APP_X="$(sanitize_num "$APP_X")" | |
| APP_Y="$(sanitize_num "$APP_Y")" | |
| APPS_X="$(sanitize_num "$APPS_X")" | |
| APPS_Y="$(sanitize_num "$APPS_Y")" | |
| mkdir -p "$EXPORT_DIR" | |
| BACKGROUND_ARG=() | |
| if [[ -n "$BG_PATH" && -f "$BG_PATH" ]]; then | |
| BG_BASENAME="$(basename "$BG_PATH")" | |
| BG_OUT="$EXPORT_DIR/$BG_BASENAME" | |
| cp -f "$BG_PATH" "$BG_OUT" | |
| BACKGROUND_ARG=(--background "$BG_OUT") | |
| fi | |
| if [[ ${#BACKGROUND_ARG[@]} -eq 0 ]]; then | |
| # Common case: background image lives in the DMG at /.background/* | |
| BG_FALLBACK="$(find "$MOUNTPOINT" -maxdepth 2 -path '*/.background/*' -type f \( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.gif' \) -print -quit 2>/dev/null || true)" | |
| if [[ -n "$BG_FALLBACK" && -f "$BG_FALLBACK" ]]; then | |
| BG_BASENAME="$(basename "$BG_FALLBACK")" | |
| BG_OUT="$EXPORT_DIR/$BG_BASENAME" | |
| cp -f "$BG_FALLBACK" "$BG_OUT" | |
| BACKGROUND_ARG=(--background "$BG_OUT") | |
| BG_PATH="$BG_FALLBACK" | |
| fi | |
| fi | |
| echo "Mounted volume: $VOLNAME" | |
| echo "Detected app: $APP_NAME" | |
| [[ -n "$BG_PATH" ]] && echo "Background: $BG_PATH" || echo "Background: (none detected)" | |
| echo | |
| echo "create-dmg flags:" | |
| echo " --volname \"$VOLNAME\"" | |
| echo " --window-pos $WIN_X $WIN_Y" | |
| echo " --window-size $WIN_W $WIN_H" | |
| echo " --icon-size $ICON_SIZE" | |
| echo " --text-size $TEXT_SIZE" | |
| if [[ ${#BACKGROUND_ARG[@]} -gt 0 ]]; then | |
| echo " --background \"${BACKGROUND_ARG[1]}\"" | |
| fi | |
| if [[ -n "$APP_X" && -n "$APP_Y" ]]; then | |
| echo " --icon \"$APP_NAME\" $APP_X $APP_Y" | |
| fi | |
| if [[ -n "$APPS_X" && -n "$APPS_Y" ]]; then | |
| echo " --app-drop-link $APPS_X $APPS_Y" | |
| fi | |
| echo | |
| echo "Example:" | |
| echo "create-dmg \\" | |
| echo " --volname \"$VOLNAME\" \\" | |
| echo " --window-pos $WIN_X $WIN_Y \\" | |
| echo " --window-size $WIN_W $WIN_H \\" | |
| echo " --icon-size $ICON_SIZE \\" | |
| echo " --text-size $TEXT_SIZE \\" | |
| if [[ ${#BACKGROUND_ARG[@]} -gt 0 ]]; then | |
| echo " --background \"${BACKGROUND_ARG[1]}\" \\" | |
| fi | |
| if [[ -n "$APP_X" && -n "$APP_Y" ]]; then | |
| echo " --icon \"$APP_NAME\" $APP_X $APP_Y \\" | |
| fi | |
| if [[ -n "$APPS_X" && -n "$APPS_Y" ]]; then | |
| echo " --app-drop-link $APPS_X $APPS_Y \\" | |
| fi | |
| echo " out.dmg /path/to/source_folder" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment