{
    "title": "How to Launch VS Code Projects via macOS Spotlight",
    "slug": "how-to-launch-vs-code-projects-via-macos-spotlight",
    "excerpt": "I recently traded my Linux setup for a MacBook Pro, but I wasn't willing to give up my custom project launchers. With a little help from Claude, I've ported my original Bash script to macOS so I can open any project instantly using Cmd + Space.",
    "body": "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.\r\n\r\nI'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.\r\n\r\nThe 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.\r\n\r\nHere is the script:\r\n\r\n```\r\n#!/usr/bin/env bash\r\n\r\nif [ \"$(uname -s)\" != \"Darwin\" ]; then\r\n    echo \"This script requires macOS.\" >&2\r\n    exit 1\r\nfi\r\n\r\nPROJECTS_DIR=\"$HOME/Projects\"\r\nAPP_DIR=\"$HOME/Applications/ProjectLaunchers\"\r\n\r\n# Safety check: ensure APP_DIR is set and not root\r\nif [ -z \"$APP_DIR\" ] || [ \"$APP_DIR\" = \"/\" ]; then\r\n    echo \"Invalid APP_DIR: '$APP_DIR'\" >&2\r\n    exit 1\r\nfi\r\n\r\n# If directory exists, empty its contents (keep the directory itself).\r\nif [ -d \"$APP_DIR\" ]; then\r\n    echo \"Cleaning existing launchers in: $APP_DIR\"\r\n    # Remove all children of APP_DIR safely\r\n    find \"$APP_DIR\" -mindepth 1 -maxdepth 1 -exec rm -rf {} +\r\nelse\r\n    mkdir -p \"$APP_DIR\"\r\nfi\r\n\r\nVSCODE_APP=\"/Applications/Visual Studio Code.app\"\r\nVSCODE_ICON=\"$VSCODE_APP/Contents/Resources/Code.icns\"\r\n\r\nif [ ! -d \"$VSCODE_APP\" ]; then\r\n    echo \"⚠️ Visual Studio Code not found in /Applications. Icons will be generic.\"\r\n    VSCODE_ICON=\"\"\r\nfi\r\n\r\nfind \"$PROJECTS_DIR\" -type f -name \".projectname\" | while read -r project_file; do\r\n    project_dir=$(dirname \"$project_file\")\r\n    project_short_name=$(head -n 1 \"$project_file\" | tr -d '[:space:]')\r\n    [ -z \"$project_short_name\" ] && continue\r\n\r\n    APP_PATH=\"$APP_DIR/$project_short_name.app\"\r\n\r\n    # Prefer building a small AppleScript-based app (avoids creating an\r\n    # Intel-only launcher binary which triggers Rosetta prompts on Apple\r\n    # Silicon). Fall back to a simple bundle if osacompile isn't present.\r\n    if command -v osacompile >/dev/null 2>&1; then\r\n        TMP_AS=\"$TMPDIR/${project_short_name// /_}-$RANDOM.applescript\"\r\n        cat > \"$TMP_AS\" <<AS\r\non run\r\n    tell application \"Visual Studio Code\"\r\n        open POSIX file \"$project_dir\"\r\n        activate\r\n    end tell\r\nend run\r\nAS\r\n        # Compile AppleScript to an app bundle\r\n        /usr/bin/osacompile -o \"$APP_PATH\" \"$TMP_AS\" >/dev/null 2>&1 || true\r\n        rm -f \"$TMP_AS\"\r\n\r\n        # If compilation failed, fall back to shell-bundle creation below\r\n        if [ ! -d \"$APP_PATH\" ]; then\r\n            echo \"Warning: osacompile failed for $project_short_name; falling back to shell launcher.\" >&2\r\n        else\r\n            # Copy VS Code icon into the compiled app and update plist\r\n            if [ -n \"$VSCODE_ICON\" ] && [ -f \"$VSCODE_ICON\" ]; then\r\n                mkdir -p \"$APP_PATH/Contents/Resources\"\r\n                cp \"$VSCODE_ICON\" \"$APP_PATH/Contents/Resources/Code.icns\"\r\n                PLIST=\"$APP_PATH/Contents/Info.plist\"\r\n                if [ -f \"$PLIST\" ] && command -v /usr/libexec/PlistBuddy >/dev/null 2>&1; then\r\n                    /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\r\n                    /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\r\n                    ID=\"com.user.$(echo \"$project_short_name\" | tr ' ' '_')\"\r\n                    /usr/libexec/PlistBuddy -c \"Set :CFBundleIdentifier $ID\" \"$PLIST\" 2>/dev/null || /usr/libexec/PlistBuddy -c \"Add :CFBundleIdentifier string $ID\" \"$PLIST\" 2>/dev/null\r\n                        /usr/libexec/PlistBuddy -c \"Set :CFBundleIconFile Code\" \"$PLIST\" 2>/dev/null || /usr/libexec/PlistBuddy -c \"Add :CFBundleIconFile string Code\" \"$PLIST\" 2>/dev/null\r\n                        /usr/libexec/PlistBuddy -c \"Set :CFBundleIconName Code\" \"$PLIST\" 2>/dev/null || /usr/libexec/PlistBuddy -c \"Add :CFBundleIconName string Code\" \"$PLIST\" 2>/dev/null\r\n                        # Remove the default applet icon if present so the new icon is used\r\n                        rm -f \"$APP_PATH/Contents/Resources/applet.icns\" \"$APP_PATH/Contents/Resources/applet.rsrc\" 2>/dev/null || true\r\n                fi\r\n\r\n                LSREGISTER=\"/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister\"\r\n                if [ -x \"$LSREGISTER\" ]; then\r\n                    \"$LSREGISTER\" -f \"$APP_PATH\" >/dev/null 2>&1 || true\r\n                fi\r\n                xattr -rd com.apple.quarantine \"$APP_PATH\" 2>/dev/null || true\r\n            fi\r\n\r\n            echo \"Created: $project_short_name.app -> $project_dir\"\r\n            continue\r\n        fi\r\n    fi\r\n\r\n    # Fallback: create a minimal bundle that runs a shell script via the system shell.\r\n    CONTENTS=\"$APP_PATH/Contents\"\r\n    mkdir -p \"$CONTENTS/MacOS\" \"$CONTENTS/Resources\"\r\n\r\n    EXECUTABLE=\"$CONTENTS/MacOS/launcher\"\r\n    cat > \"$EXECUTABLE\" <<SH\r\n#!/usr/bin/env bash\r\nopen -a \"Visual Studio Code\" -- \"$project_dir\"\r\nSH\r\n    chmod +x \"$EXECUTABLE\"\r\n\r\n    cat > \"$CONTENTS/Info.plist\" <<PLIST\r\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n<plist version=\"1.0\">\r\n<dict>\r\n    <key>CFBundleName</key>\r\n    <string>$project_short_name</string>\r\n    <key>CFBundleDisplayName</key>\r\n    <string>$project_short_name</string>\r\n    <key>CFBundleIdentifier</key>\r\n    <string>com.user.$(echo \"$project_short_name\" | tr ' ' '_')</string>\r\n    <key>CFBundleVersion</key>\r\n    <string>1.0</string>\r\n    <key>CFBundleExecutable</key>\r\n    <string>launcher</string>\r\n    <key>CFBundlePackageType</key>\r\n    <string>APPL</string>\r\n    <key>CFBundleIconFile</key>\r\n    <string>Code</string>\r\n</dict>\r\n</plist>\r\nPLIST\r\n\r\n    if [ -n \"$VSCODE_ICON\" ] && [ -f \"$VSCODE_ICON\" ]; then\r\n        cp \"$VSCODE_ICON\" \"$CONTENTS/Resources/Code.icns\"\r\n    fi\r\n\r\n    LSREGISTER=\"/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister\"\r\n    if [ -x \"$LSREGISTER\" ]; then\r\n        \"$LSREGISTER\" -f \"$APP_PATH\" >/dev/null 2>&1 || true\r\n    fi\r\n    xattr -rd com.apple.quarantine \"$APP_PATH\" 2>/dev/null || true\r\n\r\n    echo \"Created: $project_short_name.app -> $project_dir\"\r\ndone\r\n\r\necho \"✅ Done! Find your launchers in: $APP_DIR\"\r\n```",
    "tags": [],
    "published_at": "2026-03-25 10:09:24",
    "url": "https://blog.philipnewborough.co.uk/posts/how-to-launch-vs-code-projects-via-macos-spotlight",
    "featured_image": "https://blog.philipnewborough.co.uk/media/og-f237d7f4-ceed-4d3a-a577-8be556974fd2.png"
}