#!/usr/bin/env bash # # make-app.sh — build a standalone macOS .app from the MiniTerm binary. # # Each generated .app has its OWN bundle id, so macOS treats it as a separate # application: separate Dock icon, separate ⌘-Tab entry. Run this script once # per app you want. # # Usage: # ./make-app.sh \ # --name "Claude · Projekt A" \ # --bundle-id com.yourname.claude.projekta \ # --workdir ~/code/project-a \ # --command "claude" \ # [--font-size 13] \ # [--font "JetBrains Mono"] \ # [--icon ./icons/projekta.png] \ # .png/.jpg (auto-converted) or .icns # [--out ~/Applications] \ # [--close-on-exit] # close the window when the command exits # # Build the binary fresh? The script runs `swift build -c release` automatically # the first time and reuses the result for further apps. set -euo pipefail # ---- defaults ------------------------------------------------------------- NAME="" BUNDLE_ID="" WORKDIR="" COMMAND="" FONT_SIZE="13" FONT_NAME="Menlo" ICON="" OUTDIR="$HOME/Applications" CLOSE_ON_EXIT="false" # ---- parse args ----------------------------------------------------------- while [[ $# -gt 0 ]]; do case "$1" in --name) NAME="$2"; shift 2 ;; --bundle-id) BUNDLE_ID="$2"; shift 2 ;; --workdir) WORKDIR="$2"; shift 2 ;; --command) COMMAND="$2"; shift 2 ;; --font-size) FONT_SIZE="$2"; shift 2 ;; --font) FONT_NAME="$2"; shift 2 ;; --icon) ICON="$2"; shift 2 ;; --out) OUTDIR="$2"; shift 2 ;; --close-on-exit) CLOSE_ON_EXIT="true"; shift ;; -h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; *) echo "Unknown option: $1" >&2; exit 1 ;; esac done if [[ -z "$NAME" || -z "$BUNDLE_ID" ]]; then echo "Error: --name and --bundle-id are required." >&2 echo "Run './make-app.sh --help' for usage." >&2 exit 1 fi SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" # ---- build binary once ---------------------------------------------------- BIN=".build/release/MiniTerm" if [[ ! -f "$BIN" ]]; then echo "==> Building MiniTerm (release)…" swift build -c release fi # Keep workdir portable across machines/users: store paths under the current # home directory as a literal "~", so each app expands it per-user at runtime # (Swift's expandingTildeInPath). Note the shell may have already expanded a "~" # you typed on the command line into an absolute $HOME path, so we normalize that # back to "~" too. Paths outside $HOME stay absolute (genuinely machine-specific). case "$WORKDIR" in "$HOME") WORKDIR="~" ;; "$HOME"/*) WORKDIR="~/${WORKDIR#"$HOME"/}" ;; esac # ---- assemble the .app bundle --------------------------------------------- APP="$OUTDIR/$NAME.app" echo "==> Creating $APP" rm -rf "$APP" mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" cp "$BIN" "$APP/Contents/MacOS/MiniTerm" chmod +x "$APP/Contents/MacOS/MiniTerm" # Build an .icns from a square source image (PNG/JPG/…) using macOS tools. make_icns_from_image() { local src="$1" dest="$2" local iconset iconset="$(mktemp -d)/icon.iconset" mkdir -p "$iconset" # All sizes Apple expects in an .iconset local sizes=(16 32 128 256 512) for s in "${sizes[@]}"; do sips -z "$s" "$s" "$src" --out "$iconset/icon_${s}x${s}.png" >/dev/null sips -z "$((s*2))" "$((s*2))" "$src" --out "$iconset/icon_${s}x${s}@2x.png" >/dev/null done iconutil -c icns "$iconset" -o "$dest" rm -rf "$(dirname "$iconset")" } ICON_KEY="" if [[ -n "$ICON" ]]; then if [[ ! -f "$ICON" ]]; then echo " (warning: --icon file not found: $ICON; skipping icon)" >&2 elif [[ "$ICON" == *.icns ]]; then cp "$ICON" "$APP/Contents/Resources/AppIcon.icns" ICON_KEY="CFBundleIconFileAppIcon" elif command -v sips >/dev/null 2>&1 && command -v iconutil >/dev/null 2>&1; then echo "==> Converting $ICON to .icns…" if make_icns_from_image "$ICON" "$APP/Contents/Resources/AppIcon.icns"; then ICON_KEY="CFBundleIconFileAppIcon" else echo " (warning: icon conversion failed; building without icon)" >&2 fi else echo " (warning: sips/iconutil not available; pass a .icns file instead)" >&2 fi fi cat > "$APP/Contents/Info.plist" < CFBundleName${NAME} CFBundleDisplayName${NAME} CFBundleExecutableMiniTerm CFBundleIdentifier${BUNDLE_ID} CFBundlePackageTypeAPPL CFBundleVersion1 CFBundleShortVersionString1.0 LSMinimumSystemVersion13.0 NSHighResolutionCapable ${ICON_KEY} MTCommand${COMMAND} MTWorkingDirectory${WORKDIR} MTWindowTitle${NAME} MTFontSize${FONT_SIZE} MTFontName${FONT_NAME} MTCloseOnExit<${CLOSE_ON_EXIT}/> PLIST # ---- ad-hoc codesign (helps on Apple Silicon / Gatekeeper) ---------------- if command -v codesign >/dev/null 2>&1; then codesign --force --deep --sign - "$APP" >/dev/null 2>&1 \ && echo "==> Ad-hoc signed." \ || echo " (codesign skipped/failed — app still runs)" fi echo "==> Done: $APP" echo " Command : ${COMMAND:-}" echo " Workdir : ${WORKDIR:-}" echo " Font : ${FONT_NAME} ${FONT_SIZE}pt"