Skip to main content

How to Launch VS Code Projects via macOS Spotlight

A while ago, I shared a Bash script for GNOME 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"
View as: JSON Markdown

If you enjoyed this post or found it useful, you can subscribe to my RSS feed.

Similar posts

  1. GNOME menu entries for Visual Studio Code projects

    I work on a large number of code projects and I wanted a quick way to open any of my projects in Visual Studio Code, my preferred code editor. I figured the quickest way to do this under GNOME would be to create a .desktop file for each project directory.

    gnome vscode linux