# How to Launch VS Code Projects via macOS Spotlight

A while ago, I shared a [Bash script for GNOME](https://blog.philipnewborough.co.uk/posts/gnome-menu-entries-for-visual-studio-code-projects) that generated individual `.desktop` launchers for my VS Code projects. It was a massive productivity win: I could hit the **Super** key, type a project name, and jump straight into the code without digging through directories.

I've since moved over to a new Apple MacBook Pro (*honestly, perhaps the best machine I've ever owned, but that's a post for another time*) and replicating that workflow was my first priority. Since I'm still finding my "macOS legs," I teamed up with **Claude** to port the logic over.

The goal remains the same, but the implementation is native to macOS. Instead of `.desktop` files, the script now generates lightweight `.app` wrappers inside `~/Applications/ProjectLaunchers`. This allows **Spotlight (Cmd + Space)** to index my projects perfectly, giving me the same "search-and-launch" speed I had on Linux.

Here is the script:

```
#!/usr/bin/env bash

if [ "$(uname -s)" != "Darwin" ]; then
    echo "This script requires macOS." >&2
    exit 1
fi

PROJECTS_DIR="$HOME/Projects"
APP_DIR="$HOME/Applications/ProjectLaunchers"

# Safety check: ensure APP_DIR is set and not root
if [ -z "$APP_DIR" ] || [ "$APP_DIR" = "/" ]; then
    echo "Invalid APP_DIR: '$APP_DIR'" >&2
    exit 1
fi

# If directory exists, empty its contents (keep the directory itself).
if [ -d "$APP_DIR" ]; then
    echo "Cleaning existing launchers in: $APP_DIR"
    # Remove all children of APP_DIR safely
    find "$APP_DIR" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
else
    mkdir -p "$APP_DIR"
fi

VSCODE_APP="/Applications/Visual Studio Code.app"
VSCODE_ICON="$VSCODE_APP/Contents/Resources/Code.icns"

if [ ! -d "$VSCODE_APP" ]; then
    echo "⚠️ Visual Studio Code not found in /Applications. Icons will be generic."
    VSCODE_ICON=""
fi

find "$PROJECTS_DIR" -type f -name ".projectname" | while read -r project_file; do
    project_dir=$(dirname "$project_file")
    project_short_name=$(head -n 1 "$project_file" | tr -d '[:space:]')
    [ -z "$project_short_name" ] && continue

    APP_PATH="$APP_DIR/$project_short_name.app"

    # Prefer building a small AppleScript-based app (avoids creating an
    # Intel-only launcher binary which triggers Rosetta prompts on Apple
    # Silicon). Fall back to a simple bundle if osacompile isn't present.
    if command -v osacompile >/dev/null 2>&1; then
        TMP_AS="$TMPDIR/${project_short_name// /_}-$RANDOM.applescript"
        cat > "$TMP_AS" <<AS
on run
    tell application "Visual Studio Code"
        open POSIX file "$project_dir"
        activate
    end tell
end run
AS
        # Compile AppleScript to an app bundle
        /usr/bin/osacompile -o "$APP_PATH" "$TMP_AS" >/dev/null 2>&1 || true
        rm -f "$TMP_AS"

        # If compilation failed, fall back to shell-bundle creation below
        if [ ! -d "$APP_PATH" ]; then
            echo "Warning: osacompile failed for $project_short_name; falling back to shell launcher." >&2
        else
            # Copy VS Code icon into the compiled app and update plist
            if [ -n "$VSCODE_ICON" ] && [ -f "$VSCODE_ICON" ]; then
                mkdir -p "$APP_PATH/Contents/Resources"
                cp "$VSCODE_ICON" "$APP_PATH/Contents/Resources/Code.icns"
                PLIST="$APP_PATH/Contents/Info.plist"
                if [ -f "$PLIST" ] && command -v /usr/libexec/PlistBuddy >/dev/null 2>&1; then
                    /usr/libexec/PlistBuddy -c "Set :CFBundleName $project_short_name" "$PLIST" 2>/dev/null || /usr/libexec/PlistBuddy -c "Add :CFBundleName string $project_short_name" "$PLIST" 2>/dev/null
                    /usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName $project_short_name" "$PLIST" 2>/dev/null || /usr/libexec/PlistBuddy -c "Add :CFBundleDisplayName string $project_short_name" "$PLIST" 2>/dev/null
                    ID="com.user.$(echo "$project_short_name" | tr ' ' '_')"
                    /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $ID" "$PLIST" 2>/dev/null || /usr/libexec/PlistBuddy -c "Add :CFBundleIdentifier string $ID" "$PLIST" 2>/dev/null
                        /usr/libexec/PlistBuddy -c "Set :CFBundleIconFile Code" "$PLIST" 2>/dev/null || /usr/libexec/PlistBuddy -c "Add :CFBundleIconFile string Code" "$PLIST" 2>/dev/null
                        /usr/libexec/PlistBuddy -c "Set :CFBundleIconName Code" "$PLIST" 2>/dev/null || /usr/libexec/PlistBuddy -c "Add :CFBundleIconName string Code" "$PLIST" 2>/dev/null
                        # Remove the default applet icon if present so the new icon is used
                        rm -f "$APP_PATH/Contents/Resources/applet.icns" "$APP_PATH/Contents/Resources/applet.rsrc" 2>/dev/null || true
                fi

                LSREGISTER="/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister"
                if [ -x "$LSREGISTER" ]; then
                    "$LSREGISTER" -f "$APP_PATH" >/dev/null 2>&1 || true
                fi
                xattr -rd com.apple.quarantine "$APP_PATH" 2>/dev/null || true
            fi

            echo "Created: $project_short_name.app -> $project_dir"
            continue
        fi
    fi

    # Fallback: create a minimal bundle that runs a shell script via the system shell.
    CONTENTS="$APP_PATH/Contents"
    mkdir -p "$CONTENTS/MacOS" "$CONTENTS/Resources"

    EXECUTABLE="$CONTENTS/MacOS/launcher"
    cat > "$EXECUTABLE" <<SH
#!/usr/bin/env bash
open -a "Visual Studio Code" -- "$project_dir"
SH
    chmod +x "$EXECUTABLE"

    cat > "$CONTENTS/Info.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleName</key>
    <string>$project_short_name</string>
    <key>CFBundleDisplayName</key>
    <string>$project_short_name</string>
    <key>CFBundleIdentifier</key>
    <string>com.user.$(echo "$project_short_name" | tr ' ' '_')</string>
    <key>CFBundleVersion</key>
    <string>1.0</string>
    <key>CFBundleExecutable</key>
    <string>launcher</string>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleIconFile</key>
    <string>Code</string>
</dict>
</plist>
PLIST

    if [ -n "$VSCODE_ICON" ] && [ -f "$VSCODE_ICON" ]; then
        cp "$VSCODE_ICON" "$CONTENTS/Resources/Code.icns"
    fi

    LSREGISTER="/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister"
    if [ -x "$LSREGISTER" ]; then
        "$LSREGISTER" -f "$APP_PATH" >/dev/null 2>&1 || true
    fi
    xattr -rd com.apple.quarantine "$APP_PATH" 2>/dev/null || true

    echo "Created: $project_short_name.app -> $project_dir"
done

echo "✅ Done! Find your launchers in: $APP_DIR"
```