diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..9cb6d16 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,407 @@ +name: Release + +on: + push: + tags: + - "v*" + workflow_dispatch: + inputs: + tag: + description: "Release tag, for example v1.1.0" + required: true + publish: + description: "Create the GitHub release and update the stable Homebrew cask" + required: false + default: false + type: boolean + +permissions: + contents: write + +jobs: + release: + runs-on: macos-26 + timeout-minutes: 45 + + steps: + - name: Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }} + + - name: Select Xcode + run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer + + - name: Determine release version + id: version + env: + INPUT_TAG: ${{ inputs.tag }} + REF_TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + + if [ -n "$INPUT_TAG" ]; then + TAG="$INPUT_TAG" + else + TAG="$REF_TAG" + fi + + if [[ ! "$TAG" =~ ^v[0-9]+[.][0-9]+[.][0-9]+(-[0-9A-Za-z][0-9A-Za-z.-]*)?$ ]]; then + echo "Invalid release tag: $TAG" >&2 + exit 1 + fi + + VERSION="${TAG#v}" + BUNDLE_VERSION="${VERSION%%-*}" + COMMIT_SHA=$(git rev-parse HEAD) + PRERELEASE=false + if [ "$VERSION" != "$BUNDLE_VERSION" ]; then + PRERELEASE=true + fi + + { + echo "tag=$TAG" + echo "version=$VERSION" + echo "bundle_version=$BUNDLE_VERSION" + echo "commit_sha=$COMMIT_SHA" + echo "prerelease=$PRERELEASE" + } >> "$GITHUB_OUTPUT" + echo "Releasing OpenInCode $VERSION from $COMMIT_SHA" + + - name: Verify Homebrew tap credentials + if: (github.event_name == 'push' || inputs.publish) && steps.version.outputs.prerelease != 'true' + env: + HOMEBREW_REPO_SSH_KEY: ${{ secrets.HOMEBREW_REPO_SSH_KEY }} + run: | + if [ -z "$HOMEBREW_REPO_SSH_KEY" ]; then + echo "::error::HOMEBREW_REPO_SSH_KEY must be a write-enabled deploy key for sozercan/homebrew-repo." + exit 1 + fi + + - name: Run focused tests + run: ./scripts/test.sh + + - name: Configure release signing + id: signing + env: + MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} + MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }} + MACOS_KEYCHAIN_PWD: ${{ secrets.MACOS_KEYCHAIN_PWD }} + EXPECTED_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + REQUIRE_DEVELOPER_ID: ${{ github.event_name == 'push' || inputs.publish }} + run: | + set -euo pipefail + + fallback_to_adhoc() { + local reason="$1" + if [ "$REQUIRE_DEVELOPER_ID" = "true" ]; then + echo "ERROR: $reason" >&2 + exit 1 + fi + echo "::warning::$reason Falling back to ad-hoc signing; this build will not be notarized." + echo "identity=-" >> "$GITHUB_OUTPUT" + echo "mode=adhoc" >> "$GITHUB_OUTPUT" + } + + if [ "$REQUIRE_DEVELOPER_ID" = "true" ] && [ -z "$EXPECTED_TEAM_ID" ]; then + echo "ERROR: APPLE_TEAM_ID is required for public release signing." >&2 + exit 1 + fi + + if [ -z "$MACOS_CERTIFICATE" ] || [ -z "$MACOS_CERTIFICATE_PWD" ] || [ -z "$MACOS_KEYCHAIN_PWD" ]; then + fallback_to_adhoc "macOS signing secrets are not configured." + exit 0 + fi + + KEYCHAIN_PATH="$RUNNER_TEMP/openincode-signing.keychain-db" + CERT_PATH="$RUNNER_TEMP/openincode-signing.p12" + + if ! printf '%s' "$MACOS_CERTIFICATE" | base64 --decode > "$CERT_PATH"; then + fallback_to_adhoc "MACOS_CERTIFICATE is not valid base64." + exit 0 + fi + + security create-keychain -p "$MACOS_KEYCHAIN_PWD" "$KEYCHAIN_PATH" + security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" + security unlock-keychain -p "$MACOS_KEYCHAIN_PWD" "$KEYCHAIN_PATH" + + if ! security import "$CERT_PATH" -P "$MACOS_CERTIFICATE_PWD" \ + -t agg -f pkcs12 -k "$KEYCHAIN_PATH" \ + -T /usr/bin/codesign -T /usr/bin/security; then + rm -f "$CERT_PATH" + fallback_to_adhoc "The signing certificate could not be imported." + exit 0 + fi + + security set-key-partition-list -S apple-tool:,apple:,codesign: \ + -s -k "$MACOS_KEYCHAIN_PWD" "$KEYCHAIN_PATH" >/dev/null + + EXISTING_KEYCHAINS=() + while IFS= read -r keychain; do + keychain="${keychain#*\"}" + keychain="${keychain%%\"*}" + if [ -n "$keychain" ]; then + EXISTING_KEYCHAINS+=("$keychain") + fi + done < <(security list-keychains -d user) + security list-keychains -d user -s "$KEYCHAIN_PATH" "${EXISTING_KEYCHAINS[@]}" + rm -f "$CERT_PATH" + + DEVELOPER_ID=$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" 2>/dev/null \ + | awk -v team="$EXPECTED_TEAM_ID" ' + /Developer ID Application/ && (team == "" || index($0, "(" team ")") > 0) { + print $2 + exit + } + ' || true) + if [ -n "$DEVELOPER_ID" ]; then + echo "::add-mask::$DEVELOPER_ID" + echo "identity=$DEVELOPER_ID" >> "$GITHUB_OUTPUT" + echo "mode=developer-id" >> "$GITHUB_OUTPUT" + exit 0 + fi + + APPLE_DEVELOPMENT=$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" 2>/dev/null \ + | awk '/Apple Development/ { print $2; exit }' || true) + if [ -n "$APPLE_DEVELOPMENT" ] && [ "$REQUIRE_DEVELOPER_ID" != "true" ]; then + echo "::add-mask::$APPLE_DEVELOPMENT" + echo "::warning::Using Apple Development signing; this build will not be notarized." + echo "identity=$APPLE_DEVELOPMENT" >> "$GITHUB_OUTPUT" + echo "mode=development" >> "$GITHUB_OUTPUT" + exit 0 + fi + + fallback_to_adhoc "The imported keychain has no usable Developer ID Application identity." + + - name: Build and sign release app + id: app + env: + VERSION: ${{ steps.version.outputs.version }} + BUNDLE_VERSION: ${{ steps.version.outputs.bundle_version }} + SIGNING_IDENTITY: ${{ steps.signing.outputs.identity }} + SIGNING_MODE: ${{ steps.signing.outputs.mode }} + run: | + set -euo pipefail + + DERIVED_DATA="$RUNNER_TEMP/OpenInCodeDerivedData" + xcodebuild \ + -project "Open in Code.xcodeproj" \ + -scheme "Open in Code" \ + -configuration Release \ + -destination "generic/platform=macOS" \ + -derivedDataPath "$DERIVED_DATA" \ + ARCHS="arm64 x86_64" \ + ONLY_ACTIVE_ARCH=NO \ + CODE_SIGNING_ALLOWED=NO \ + CODE_SIGNING_REQUIRED=NO \ + clean build + + APP_PATH="$DERIVED_DATA/Build/Products/Release/Open in Code.app" + PLIST_PATH="$APP_PATH/Contents/Info.plist" + EXECUTABLE_PATH="$APP_PATH/Contents/MacOS/Open in Code" + + test -d "$APP_PATH" + /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $BUNDLE_VERSION" "$PLIST_PATH" + /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $GITHUB_RUN_NUMBER" "$PLIST_PATH" + + ARCHITECTURES=$(lipo -archs "$EXECUTABLE_PATH") + echo "Architectures: $ARCHITECTURES" + [[ "$ARCHITECTURES" == *arm64* ]] + [[ "$ARCHITECTURES" == *x86_64* ]] + + if [ "$SIGNING_MODE" = "developer-id" ]; then + codesign --force --options runtime --timestamp \ + --entitlements "Open in Code.entitlements" \ + --sign "$SIGNING_IDENTITY" "$APP_PATH" + elif [ "$SIGNING_MODE" = "development" ]; then + codesign --force --options runtime --timestamp=none \ + --entitlements "Open in Code.entitlements" \ + --sign "$SIGNING_IDENTITY" "$APP_PATH" + else + codesign --force --options runtime --timestamp=none \ + --entitlements "Open in Code.entitlements" \ + --sign - "$APP_PATH" + fi + + codesign --verify --deep --strict --verbose=4 "$APP_PATH" + ENTITLEMENTS=$(codesign -d --entitlements :- "$APP_PATH" 2>/dev/null) + grep -F 'com.apple.security.automation.apple-events' <<< "$ENTITLEMENTS" >/dev/null + + echo "path=$APP_PATH" >> "$GITHUB_OUTPUT" + + - name: Notarize Developer ID app + if: steps.signing.outputs.mode == 'developer-id' && (github.event_name == 'push' || inputs.publish) + env: + APP_PATH: ${{ steps.app.outputs.path }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + set -euo pipefail + + if [ -z "$APPLE_ID" ] || [ -z "$APPLE_APP_SPECIFIC_PASSWORD" ] || [ -z "$APPLE_TEAM_ID" ]; then + echo "Developer ID signing requires APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, and APPLE_TEAM_ID for notarization." >&2 + exit 1 + fi + + NOTARY_ZIP="$RUNNER_TEMP/OpenInCode-notarization.zip" + ditto -c -k --sequesterRsrc --keepParent "$APP_PATH" "$NOTARY_ZIP" + xcrun notarytool submit "$NOTARY_ZIP" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_APP_SPECIFIC_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" \ + --wait + xcrun stapler staple "$APP_PATH" + xcrun stapler validate "$APP_PATH" + spctl --assess --type execute --verbose=4 "$APP_PATH" + + - name: Package release + id: artifact + env: + APP_PATH: ${{ steps.app.outputs.path }} + VERSION: ${{ steps.version.outputs.version }} + TAG: ${{ steps.version.outputs.tag }} + run: | + set -euo pipefail + + FILE="OpenInCode-v${VERSION}.zip" + ditto -c -k --sequesterRsrc --keepParent "$APP_PATH" "$FILE" + unzip -l "$FILE" > "$RUNNER_TEMP/openincode-zip-contents.txt" + grep -F 'Open in Code.app/Contents/MacOS/Open in Code' \ + "$RUNNER_TEMP/openincode-zip-contents.txt" >/dev/null + + SHA256=$(shasum -a 256 "$FILE" | awk '{print $1}') + { + echo "filename=$FILE" + echo "sha256=$SHA256" + echo "tag=$TAG" + } >> "$GITHUB_OUTPUT" + + - name: Upload workflow artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ steps.artifact.outputs.filename }} + path: ${{ steps.artifact.outputs.filename }} + retention-days: 30 + + - name: Create immutable GitHub release + id: release + if: github.event_name == 'push' || inputs.publish + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ steps.artifact.outputs.tag }} + FILE: ${{ steps.artifact.outputs.filename }} + IS_PRERELEASE: ${{ steps.version.outputs.prerelease }} + run: | + set -euo pipefail + + asset_exists() { + gh release view "$TAG" --json assets --jq '.assets[] | [.name, .state] | @tsv' \ + | awk -F '\t' -v file="$FILE" ' + $1 == file && $2 == "uploaded" { found = 1 } + END { exit(found ? 0 : 1) } + ' + } + + if ! gh release view "$TAG" >/dev/null 2>&1; then + if [ "$IS_PRERELEASE" = "true" ]; then + gh release create "$TAG" "$FILE" \ + --target "${{ steps.version.outputs.commit_sha }}" \ + --title "$TAG" \ + --generate-notes \ + --prerelease || true + else + gh release create "$TAG" "$FILE" \ + --target "${{ steps.version.outputs.commit_sha }}" \ + --title "$TAG" \ + --generate-notes || true + fi + + if ! gh release view "$TAG" >/dev/null 2>&1; then + echo "Failed to create release $TAG." >&2 + exit 1 + fi + fi + + if asset_exists; then + echo "Release $TAG already contains immutable asset $FILE." + else + echo "Release $TAG is missing $FILE; restoring the asset without clobbering." + if ! gh release upload "$TAG" "$FILE"; then + echo "Asset upload raced with another recovery; verifying the published asset." + asset_exists || { + echo "Release $TAG still has no uploaded $FILE asset." >&2 + exit 1 + } + fi + fi + + rm -f "$FILE" + gh release download "$TAG" --pattern "$FILE" --dir . + PUBLISHED_SHA256=$(shasum -a 256 "$FILE" | awk '{print $1}') + echo "sha256=$PUBLISHED_SHA256" >> "$GITHUB_OUTPUT" + + - name: Checkout Homebrew tap + if: (github.event_name == 'push' || inputs.publish) && steps.version.outputs.prerelease != 'true' + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + repository: sozercan/homebrew-repo + ssh-key: ${{ secrets.HOMEBREW_REPO_SSH_KEY }} + path: homebrew-repo + fetch-depth: 0 + + - name: Publish Homebrew cask + if: (github.event_name == 'push' || inputs.publish) && steps.version.outputs.prerelease != 'true' + env: + VERSION: ${{ steps.version.outputs.version }} + SHA256: ${{ steps.release.outputs.sha256 }} + TAG: ${{ steps.artifact.outputs.tag }} + run: | + set -euo pipefail + + cd homebrew-repo + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + for attempt in 1 2 3 4 5; do + git fetch origin main + git reset --hard origin/main + + CURRENT_VERSION="0" + if [ -f Casks/open-in-code.rb ]; then + CURRENT_VERSION=$(sed -n 's/^ version "\([^"]*\)"/\1/p' Casks/open-in-code.rb | head -1) + fi + + if ruby -e ' + require "rubygems" + current = Gem::Version.new(ARGV.fetch(0)) + candidate = Gem::Version.new(ARGV.fetch(1)) + exit(current > candidate ? 0 : 1) + ' "$CURRENT_VERSION" "$VERSION"; then + echo "Homebrew already contains newer version $CURRENT_VERSION; skipping stale release $VERSION." + exit 0 + fi + + ../scripts/render-homebrew-cask.sh \ + "$VERSION" \ + "$SHA256" \ + Casks/open-in-code.rb + git add Casks/open-in-code.rb + + if git diff --cached --quiet; then + echo "Homebrew cask is already up to date." + exit 0 + fi + + git commit -s -m "Update open-in-code to ${TAG}" + if git push origin HEAD:main; then + exit 0 + fi + + echo "Homebrew push attempt $attempt raced with another update; retrying..." + sleep $((attempt * 5)) + done + + echo "Failed to update the Homebrew cask after retries." >&2 + exit 1 diff --git a/.gitignore b/.gitignore index 86f21d8..6cf1eee 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,5 @@ fastlane/screenshots # https://github.com/johnno1962/injectionforxcode iOSInjectionProject/ + +.DS_Store diff --git a/Info.plist b/Info.plist index 67a11e4..1ad0840 100755 --- a/Info.plist +++ b/Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable Open in Code CFBundleIconFile - icon.icns + vscode CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion @@ -17,19 +17,19 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0 + 1.1.0 CFBundleSignature ???? CFBundleVersion - 1 + 2 LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion - 10.7 + 12.0 LSUIElement 1 - NSMainNibFile - MainMenu + NSAppleEventsUsageDescription + Open in Code uses Finder to determine which folder or selected item to open in Visual Studio Code. NSPrincipalClass NSApplication diff --git a/Open in Code.entitlements b/Open in Code.entitlements new file mode 100644 index 0000000..49ad0bb --- /dev/null +++ b/Open in Code.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.automation.apple-events + + + diff --git a/Open in Code.xcodeproj/project.pbxproj b/Open in Code.xcodeproj/project.pbxproj index 1c1d459..87ebeb5 100755 --- a/Open in Code.xcodeproj/project.pbxproj +++ b/Open in Code.xcodeproj/project.pbxproj @@ -3,17 +3,17 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ - 69C8AA71106E8F5800C4DE0D /* Finder.app in Resources */ = {isa = PBXBuildFile; fileRef = 69C8AA70106E8F5800C4DE0D /* Finder.app */; }; 69C8AA89106EE02F00C4DE0D /* Finder.app in Sources */ = {isa = PBXBuildFile; fileRef = 69C8AA70106E8F5800C4DE0D /* Finder.app */; }; 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; }; 8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; }; + C89E34F62FBA29F900F208FE /* OpenInCodeCore.m in Sources */ = {isa = PBXBuildFile; fileRef = C89E34F32FBA29F900F208FE /* OpenInCodeCore.m */; }; 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; C82F4C8C1D31B01E0084C3F5 /* ScriptingBridge.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 69C8A98D106E818900C4DE0D /* ScriptingBridge.framework */; }; - C82F4C8E1D31B82C0084C3F5 /* icon.icns in Resources */ = {isa = PBXBuildFile; fileRef = C82F4C8D1D31B82C0084C3F5 /* icon.icns */; }; + C89E34F12FBA29F900F208FE /* vscode.icon in Resources */ = {isa = PBXBuildFile; fileRef = C89E34F02FBA29F900F208FE /* vscode.icon */; }; /* End PBXBuildFile section */ /* Begin PBXBuildRule section */ @@ -22,10 +22,13 @@ compilerSpec = com.apple.compilers.proxy.script; filePatterns = "*.app"; fileType = pattern.proxy; + inputFiles = ( + ); isEditable = 1; outputFiles = ( "$(DERIVED_FILES_DIR)/$(INPUT_FILE_BASE).h", ); + runOncePerArchitecture = 0; script = "sdef \"$INPUT_FILE_PATH\" | sdp -fh -o \"$DERIVED_FILES_DIR\" --basename \"$INPUT_FILE_BASE\" --bundleid `defaults read \"$INPUT_FILE_PATH/Contents/Info\" CFBundleIdentifier`"; }; /* End PBXBuildRule section */ @@ -54,7 +57,10 @@ 69C8AA70106E8F5800C4DE0D /* Finder.app */ = {isa = PBXFileReference; lastKnownFileType = wrapper.application; name = Finder.app; path = /System/Library/CoreServices/Finder.app; sourceTree = ""; }; 8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 8D1107320486CEB800E47090 /* Open in Code.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Open in Code.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - C82F4C8D1D31B82C0084C3F5 /* icon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = icon.icns; sourceTree = ""; }; + C89E34F02FBA29F900F208FE /* vscode.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = vscode.icon; sourceTree = ""; }; + C89E34F22FBA29F900F208FE /* OpenInCodeCore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OpenInCodeCore.h; sourceTree = ""; }; + C89E34F32FBA29F900F208FE /* OpenInCodeCore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OpenInCodeCore.m; sourceTree = ""; }; + C89E34F52FBA29F900F208FE /* Open in Code.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Open in Code.entitlements"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -114,6 +120,9 @@ children = ( 32CA4F630368D1EE00C91783 /* Open in Code_Prefix.pch */, 29B97316FDCFA39411CA2CEA /* main.m */, + C89E34F22FBA29F900F208FE /* OpenInCodeCore.h */, + C89E34F32FBA29F900F208FE /* OpenInCodeCore.m */, + C89E34F52FBA29F900F208FE /* Open in Code.entitlements */, ); name = "Other Sources"; sourceTree = ""; @@ -121,9 +130,9 @@ 29B97317FDCFA39411CA2CEA /* Resources */ = { isa = PBXGroup; children = ( - C82F4C8D1D31B82C0084C3F5 /* icon.icns */, 8D1107310486CEB800E47090 /* Info.plist */, 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */, + C89E34F02FBA29F900F208FE /* vscode.icon */, ); name = Resources; sourceTree = ""; @@ -155,7 +164,7 @@ dependencies = ( ); name = "Open in Code"; - productInstallPath = "$(HOME)/Applications"; + productInstallPath = /Applications; productName = "Open In Code"; productReference = 8D1107320486CEB800E47090 /* Open in Code.app */; productType = "com.apple.product-type.application"; @@ -166,18 +175,14 @@ 29B97313FDCFA39411CA2CEA /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0800; - TargetAttributes = { - 8D1107260486CEB800E47090 = { - DevelopmentTeam = VURRGRYW45; - }; - }; + LastUpgradeCheck = 2700; }; buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "Open in Code" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 1; knownRegions = ( + English, en, ); mainGroup = 29B97314FDCFA39411CA2CEA /* Open In Code */; @@ -194,9 +199,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - C82F4C8E1D31B82C0084C3F5 /* icon.icns in Resources */, 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */, - 69C8AA71106E8F5800C4DE0D /* Finder.app in Resources */, + C89E34F12FBA29F900F208FE /* vscode.icon in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -209,6 +213,7 @@ files = ( 69C8AA89106EE02F00C4DE0D /* Finder.app in Sources */, 8D11072D0486CEB800E47090 /* main.m in Sources */, + C89E34F62FBA29F900F208FE /* OpenInCodeCore.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -229,22 +234,23 @@ C01FCF4B08A954540054247B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - CODE_SIGN_IDENTITY = ""; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = ""; + ASSETCATALOG_COMPILER_APPICON_NAME = vscode; + CLANG_ENABLE_OBJC_ARC = NO; + CODE_SIGN_ENTITLEMENTS = "Open in Code.entitlements"; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; "COPY_PHASE_STRIP[sdk=*]" = NO; + ENABLE_HARDENED_RUNTIME = YES; GCC_DYNAMIC_NO_PIC = NO; GCC_ENABLE_OBJC_EXCEPTIONS = YES; GCC_OPTIMIZATION_LEVEL = 0; - GCC_VERSION = ""; INFOPLIST_FILE = Info.plist; - INSTALL_PATH = /unsigned; - MACOSX_DEPLOYMENT_TARGET = 10.8; - OTHER_CODE_SIGN_FLAGS = ""; + INSTALL_PATH = /Applications; + MACOSX_DEPLOYMENT_TARGET = 12.0; PRODUCT_BUNDLE_IDENTIFIER = com.sertacozercan.openincode; PRODUCT_NAME = "Open in Code"; - PROVISIONING_PROFILE = ""; WRAPPER_EXTENSION = app; }; name = Debug; @@ -252,19 +258,21 @@ C01FCF4C08A954540054247B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - CODE_SIGN_IDENTITY = ""; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = ""; + ASSETCATALOG_COMPILER_APPICON_NAME = vscode; + CLANG_ENABLE_OBJC_ARC = NO; + CODE_SIGN_ENTITLEMENTS = "Open in Code.entitlements"; + CODE_SIGN_IDENTITY = "Developer ID Application"; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = VURRGRYW45; + ENABLE_HARDENED_RUNTIME = YES; GCC_ENABLE_OBJC_EXCEPTIONS = YES; GCC_GENERATE_DEBUGGING_SYMBOLS = NO; - GCC_VERSION = ""; INFOPLIST_FILE = Info.plist; - INSTALL_PATH = /unsigned; - MACOSX_DEPLOYMENT_TARGET = 10.8; - OTHER_CODE_SIGN_FLAGS = ""; + INSTALL_PATH = /Applications; + MACOSX_DEPLOYMENT_TARGET = 12.0; PRODUCT_BUNDLE_IDENTIFIER = com.sertacozercan.openincode; PRODUCT_NAME = "Open in Code"; - PROVISIONING_PROFILE = ""; WRAPPER_EXTENSION = app; }; name = Release; @@ -272,6 +280,7 @@ C01FCF4F08A954540054247B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_EMPTY_BODY = YES; @@ -289,7 +298,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INSTALL_PATH = ""; - MACOSX_DEPLOYMENT_TARGET = 10.8; + MACOSX_DEPLOYMENT_TARGET = 12.0; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; }; @@ -298,6 +307,7 @@ C01FCF5008A954540054247B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_EMPTY_BODY = YES; @@ -314,7 +324,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INSTALL_PATH = ""; - MACOSX_DEPLOYMENT_TARGET = 10.8; + MACOSX_DEPLOYMENT_TARGET = 12.0; SDKROOT = macosx; }; name = Release; diff --git a/Open in Code.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Open in Code.xcodeproj/project.xcworkspace/contents.xcworkspacedata index d6eb371..919434a 100755 --- a/Open in Code.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/Open in Code.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/Open in Code.xcodeproj/xcshareddata/xcschemes/Open in Code.xcscheme b/Open in Code.xcodeproj/xcshareddata/xcschemes/Open in Code.xcscheme new file mode 100644 index 0000000..cfed17d --- /dev/null +++ b/Open in Code.xcodeproj/xcshareddata/xcschemes/Open in Code.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OpenInCodeCore.h b/OpenInCodeCore.h new file mode 100644 index 0000000..f3489e4 --- /dev/null +++ b/OpenInCodeCore.h @@ -0,0 +1,7 @@ +#import + +extern NSString * const OICVSCodeBundleIdentifier; +extern NSString * const OICVSCodeInsidersBundleIdentifier; + +NSArray *OICPreferredVSCodeBundleIdentifiers(void); +NSString *OICPathForFinderURL(NSURL *url); diff --git a/OpenInCodeCore.m b/OpenInCodeCore.m new file mode 100644 index 0000000..1fcf838 --- /dev/null +++ b/OpenInCodeCore.m @@ -0,0 +1,58 @@ +#import "OpenInCodeCore.h" + +NSString * const OICVSCodeBundleIdentifier = @"com.microsoft.VSCode"; +NSString * const OICVSCodeInsidersBundleIdentifier = @"com.microsoft.VSCodeInsiders"; + +NSArray *OICPreferredVSCodeBundleIdentifiers(void) +{ + return @[OICVSCodeBundleIdentifier, OICVSCodeInsidersBundleIdentifier]; +} + +NSString *OICPathForFinderURL(NSURL *url) +{ + if (url == nil || ![url isFileURL]) { + return nil; + } + + NSNumber *isAliasFile = nil; + NSNumber *isSymbolicLink = nil; + [url getResourceValue:&isAliasFile forKey:NSURLIsAliasFileKey error:nil]; + [url getResourceValue:&isSymbolicLink forKey:NSURLIsSymbolicLinkKey error:nil]; + if ([isAliasFile boolValue] && ![isSymbolicLink boolValue]) { + NSData *bookmark = [NSURL bookmarkDataWithContentsOfURL:url error:nil]; + if (bookmark == nil) { + return nil; + } + + NSURL *resolvedURL = [NSURL URLByResolvingBookmarkData:bookmark + options:NSURLBookmarkResolutionWithoutUI + relativeToURL:nil + bookmarkDataIsStale:nil + error:nil]; + if (resolvedURL == nil) { + return nil; + } + url = resolvedURL; + } + + NSString *path = [[url path] stringByExpandingTildeInPath]; + if ([path length] == 0) { + return nil; + } + + BOOL isDirectory = NO; + if (![[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDirectory]) { + return nil; + } + + NSURL *packageCheckURL = [url URLByResolvingSymlinksInPath]; + NSNumber *isPackageValue = nil; + [packageCheckURL getResourceValue:&isPackageValue forKey:NSURLIsPackageKey error:nil]; + BOOL isPackage = [isPackageValue boolValue]; + + if (!isDirectory || isPackage) { + path = [path stringByDeletingLastPathComponent]; + } + + return [path length] > 0 ? path : nil; +} diff --git a/README.md b/README.md index cc64efd..c759834 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,68 @@ # OpenInCode -:open_file_folder: Finder toolbar app to open current folder in Visual Studio Code - -Installation and usage: -- Download app for [Light](https://github.com/sozercan/OpenInCode/releases/download/v1.0/OpenInCode.app.zip) or [Dark](https://github.com/sozercan/OpenInCode/releases/download/v1.0/OpenInCodeDark.zip) mode or using `brew cask install open-in-code` -(if using [VS Code Insiders](https://code.visualstudio.com/insiders), download [this](https://github.com/sozercan/OpenInCode/releases/download/v1.0/OpenInCodeInsiders.app.zip) instead) -- Move Open in Code.app to /Applications -- Go to /Applications -- While holding Command key, drag Open in Code.app to Finder toolbar -- Right-click Open in Code.app and click Open. When the security dialog prompt appears, click Open. -- Once Visual Studio Code new window is opened, close it. -- Navigate to a folder you want to open with Visual Studio Code -- Click on the toolbar icon -- Folder will open with Visual Studio Code - -![](http://i.imgur.com/F5ZrCmS.gif) + +Finder toolbar app that opens the current Finder folder—or the folder containing the selected file—in Visual Studio Code. + +OpenInCode prefers stable Visual Studio Code and falls back to Visual Studio Code Insiders when stable is not installed. + +## Requirements + +- macOS 12 or newer +- Visual Studio Code or Visual Studio Code Insiders +- Xcode 26 or newer to build the current Icon Composer asset + +## Install + +Install the latest release from the Homebrew tap: + +```sh +brew install --cask sozercan/repo/open-in-code +``` + +Release archives are also available from the [GitHub Releases](https://github.com/sozercan/OpenInCode/releases) page. + +To build from source: + +1. Clone this repository. +2. Open `Open in Code.xcodeproj` in Xcode. +3. Build the Debug configuration for local use, or select a Developer ID Application certificate before archiving Release. +4. Copy `Open in Code.app` to `/Applications`. +5. Hold Command and drag the app from `/Applications` to a Finder toolbar. +6. Click the toolbar icon while viewing a folder or selecting a file. + +The first use asks for permission to control Finder. If permission was denied, enable **Open in Code → Finder** here: + +- macOS 13 or newer: **System Settings → Privacy & Security → Automation** +- macOS 12: **System Preferences → Security & Privacy → Privacy → Automation** + +Release archives are configured for hardened runtime and the project publisher team. Fork maintainers should override `DEVELOPMENT_TEAM` with their own team and provide a Developer ID Application certificate. Notarize public release artifacts before distribution. + +## Development + +Run the focused path and editor-selection tests: + +```sh +./scripts/test.sh +``` + +Run a clean unsigned compiler verification: + +```sh +xcodebuild \ + -project "Open in Code.xcodeproj" \ + -scheme "Open in Code" \ + -configuration Release \ + CODE_SIGNING_ALLOWED=NO \ + clean build +``` + +## Publishing a release + +Pushing a `v*` tag runs `.github/workflows/release.yml`. The workflow builds a universal app, signs it, creates a GitHub release, and updates `Casks/open-in-code.rb` in [`sozercan/homebrew-repo`](https://github.com/sozercan/homebrew-repo). + +Public releases require all of these Actions secrets: + +- `HOMEBREW_REPO_SSH_KEY`: the private half of a write-enabled deploy key for `sozercan/homebrew-repo` +- `MACOS_CERTIFICATE`, `MACOS_CERTIFICATE_PWD`, `MACOS_KEYCHAIN_PWD`: a Developer ID Application PKCS#12 and its temporary keychain credentials +- `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, `APPLE_TEAM_ID`: notarization credentials + +Tag pushes and manual runs with `publish=true` fail unless Developer ID signing and notarization succeed. A manual run with `publish=false` may use ad-hoc signing for validation, but it never creates a GitHub release or updates Homebrew. Prerelease tags create GitHub prereleases but do not replace the stable Homebrew cask. diff --git a/Tests/OpenInCodeCoreTests.m b/Tests/OpenInCodeCoreTests.m new file mode 100644 index 0000000..4ec5161 --- /dev/null +++ b/Tests/OpenInCodeCoreTests.m @@ -0,0 +1,85 @@ +#import +#include +#import "OpenInCodeCore.h" + +static void assertTrue(BOOL condition, NSString *message) +{ + if (!condition) { + NSLog(@"FAIL: %@", message); + exit(1); + } +} + +int main(void) +{ + @autoreleasepool { + NSArray *bundleIdentifiers = OICPreferredVSCodeBundleIdentifiers(); + assertTrue([bundleIdentifiers count] == 2, @"expected two VS Code channels"); + assertTrue([bundleIdentifiers[0] isEqualToString:OICVSCodeBundleIdentifier], @"stable VS Code must be preferred"); + assertTrue([bundleIdentifiers[1] isEqualToString:OICVSCodeInsidersBundleIdentifier], @"Insiders must be the fallback"); + + assertTrue(OICPathForFinderURL(nil) == nil, @"nil URL must return nil"); + assertTrue(OICPathForFinderURL([NSURL URLWithString:@"https://example.com"]) == nil, @"non-file URL must return nil"); + + NSString *temporaryRoot = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSUUID UUID] UUIDString]]; + NSString *folderPath = [temporaryRoot stringByAppendingPathComponent:@"Project"]; + NSString *filePath = [folderPath stringByAppendingPathComponent:@"README.md"]; + NSString *packagePath = [temporaryRoot stringByAppendingPathComponent:@"Example.app"]; + NSString *directorySymlinkPath = [temporaryRoot stringByAppendingPathComponent:@"LinkedProject"]; + NSURL *folderAliasURL = [NSURL fileURLWithPath:[temporaryRoot stringByAppendingPathComponent:@"Project Folder.alias"]]; + NSURL *fileAliasURL = [NSURL fileURLWithPath:[temporaryRoot stringByAppendingPathComponent:@"Project File.alias"]]; + NSString *deletedTargetPath = [temporaryRoot stringByAppendingPathComponent:@"Deleted Target.txt"]; + NSURL *brokenAliasURL = [NSURL fileURLWithPath:[temporaryRoot stringByAppendingPathComponent:@"Broken Target.alias"]]; + + NSFileManager *fileManager = [NSFileManager defaultManager]; + assertTrue([fileManager createDirectoryAtPath:folderPath withIntermediateDirectories:YES attributes:nil error:nil], @"create test folder"); + assertTrue([@"test" writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:nil], @"create test file"); + assertTrue([@"delete me" writeToFile:deletedTargetPath atomically:YES encoding:NSUTF8StringEncoding error:nil], @"create deleted alias target"); + assertTrue([fileManager createDirectoryAtPath:packagePath withIntermediateDirectories:YES attributes:nil error:nil], @"create test package"); + assertTrue([fileManager createSymbolicLinkAtPath:directorySymlinkPath withDestinationPath:folderPath error:nil], @"create directory symlink"); + + NSData *folderBookmark = [[NSURL fileURLWithPath:folderPath isDirectory:YES] + bookmarkDataWithOptions:NSURLBookmarkCreationSuitableForBookmarkFile + includingResourceValuesForKeys:nil + relativeToURL:nil + error:nil]; + NSData *fileBookmark = [[NSURL fileURLWithPath:filePath] + bookmarkDataWithOptions:NSURLBookmarkCreationSuitableForBookmarkFile + includingResourceValuesForKeys:nil + relativeToURL:nil + error:nil]; + NSData *deletedTargetBookmark = [[NSURL fileURLWithPath:deletedTargetPath] + bookmarkDataWithOptions:NSURLBookmarkCreationSuitableForBookmarkFile + includingResourceValuesForKeys:nil + relativeToURL:nil + error:nil]; + assertTrue(folderBookmark != nil, @"create folder bookmark data"); + assertTrue(fileBookmark != nil, @"create file bookmark data"); + assertTrue(deletedTargetBookmark != nil, @"create deleted-target bookmark data"); + assertTrue([NSURL writeBookmarkData:folderBookmark toURL:folderAliasURL options:0 error:nil], @"create folder alias"); + assertTrue([NSURL writeBookmarkData:fileBookmark toURL:fileAliasURL options:0 error:nil], @"create file alias"); + assertTrue([NSURL writeBookmarkData:deletedTargetBookmark toURL:brokenAliasURL options:0 error:nil], @"create broken alias"); + assertTrue([fileManager removeItemAtPath:deletedTargetPath error:nil], @"delete alias target"); + + NSURL *resolvedFolderAliasURL = [NSURL URLByResolvingAliasFileAtURL:folderAliasURL options:NSURLBookmarkResolutionWithoutUI error:nil]; + NSURL *resolvedFileAliasURL = [NSURL URLByResolvingAliasFileAtURL:fileAliasURL options:NSURLBookmarkResolutionWithoutUI error:nil]; + assertTrue(resolvedFolderAliasURL != nil, @"resolve folder alias"); + assertTrue(resolvedFileAliasURL != nil, @"resolve file alias"); + NSString *resolvedFolderAliasPath = [resolvedFolderAliasURL path]; + NSString *resolvedFileAliasParentPath = [[resolvedFileAliasURL path] stringByDeletingLastPathComponent]; + + assertTrue([OICPathForFinderURL([NSURL fileURLWithPath:folderPath]) isEqualToString:folderPath], @"folder should open itself"); + assertTrue([OICPathForFinderURL([NSURL fileURLWithPath:filePath]) isEqualToString:folderPath], @"file should open its parent"); + assertTrue([OICPathForFinderURL([NSURL fileURLWithPath:packagePath]) isEqualToString:temporaryRoot], @"Finder package should open its parent"); + assertTrue([OICPathForFinderURL([NSURL fileURLWithPath:directorySymlinkPath]) isEqualToString:directorySymlinkPath], @"directory symlink should open the linked directory"); + assertTrue([OICPathForFinderURL(folderAliasURL) isEqualToString:resolvedFolderAliasPath], @"folder alias should resolve to its target"); + assertTrue([OICPathForFinderURL(fileAliasURL) isEqualToString:resolvedFileAliasParentPath], @"file alias should resolve to its target parent"); + assertTrue(OICPathForFinderURL(brokenAliasURL) == nil, @"broken alias must return nil"); + assertTrue(OICPathForFinderURL([NSURL fileURLWithPath:[temporaryRoot stringByAppendingPathComponent:@"missing"]]) == nil, @"missing item must return nil"); + + assertTrue([fileManager removeItemAtPath:temporaryRoot error:nil], @"remove test files"); + NSLog(@"OpenInCodeCoreTests: PASS"); + } + + return 0; +} diff --git a/icon.icns b/icon.icns deleted file mode 100644 index 9ea6655..0000000 Binary files a/icon.icns and /dev/null differ diff --git a/main.m b/main.m index 9015a04..8f7da68 100755 --- a/main.m +++ b/main.m @@ -7,54 +7,176 @@ // #import +#import #import "Finder.h" +#import "OpenInCodeCore.h" -NSString* getPathToFrontFinderWindow(){ - - FinderApplication* finder = [SBApplication applicationWithBundleIdentifier:@"com.apple.Finder"]; - - FinderItem *target = [(NSArray*)[[finder selection]get] firstObject]; - if (target == nil){ - target = [[[[finder FinderWindows] firstObject] target] get]; +static NSString * const FinderAutomationPermissionMessage = @"Open the Automation privacy settings and allow Open in Code to control Finder, then try again."; + +@interface OICScriptingBridgeErrorHandler : NSObject { + NSError *_lastError; +} + +@property(nonatomic, retain) NSError *lastError; + +@end + +@implementation OICScriptingBridgeErrorHandler + +@synthesize lastError = _lastError; + +- (id)eventDidFail:(const AppleEvent *)event withError:(NSError *)error +{ + (void)event; + [self setLastError:error]; + return nil; +} + +- (void)dealloc +{ + [_lastError release]; + [super dealloc]; +} + +@end + +static void showErrorAlert(NSString *message, NSString *informativeText) +{ + [NSApplication sharedApplication]; + [NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory]; + [NSApp activateIgnoringOtherApps:YES]; + + NSAlert *alert = [[[NSAlert alloc] init] autorelease]; + [alert setAlertStyle:NSAlertStyleCritical]; + [alert setMessageText:message]; + [alert setInformativeText:informativeText]; + [alert addButtonWithTitle:@"OK"]; + [alert runModal]; +} + +static NSString *pathToFrontFinderLocation(NSString **errorMessage) +{ + @try { + FinderApplication *finder = [SBApplication applicationWithBundleIdentifier:@"com.apple.Finder"]; + OICScriptingBridgeErrorHandler *errorHandler = [[[OICScriptingBridgeErrorHandler alloc] init] autorelease]; + [finder setDelegate:errorHandler]; + + FinderItem *target = [(NSArray *)[[finder selection] get] firstObject]; + + if (target == nil) { + target = [[[[finder FinderWindows] firstObject] target] get]; + } + + NSString *targetURLString = [target URL]; + if ([targetURLString length] == 0) { + if (errorMessage != NULL) { + if ([errorHandler lastError] != nil) { + *errorMessage = FinderAutomationPermissionMessage; + } else { + *errorMessage = @"Open a Finder window for a local folder and try again."; + } + } + return nil; + } + + NSString *path = OICPathForFinderURL([NSURL URLWithString:targetURLString]); + if ([path length] == 0 && errorMessage != NULL) { + *errorMessage = @"The selected Finder item does not have an accessible local path."; + } + + return path; + } + @catch (NSException *exception) { + if (errorMessage != NULL) { + *errorMessage = FinderAutomationPermissionMessage; + } + return nil; } - - NSURL* url =[NSURL URLWithString:target.URL]; - NSError* error; - NSData* bookmark = [NSURL bookmarkDataWithContentsOfURL:url error:nil]; - NSURL* fullUrl = [NSURL URLByResolvingBookmarkData:bookmark - options:NSURLBookmarkResolutionWithoutUI - relativeToURL:nil - bookmarkDataIsStale:nil - error:&error]; - if(fullUrl != nil){ - url = fullUrl; +} + +static BOOL openPathInPreferredVSCode(NSString *path, NSString **errorMessage) +{ + if ([path length] == 0) { + if (errorMessage != NULL) { + *errorMessage = @"No folder path was available to open."; + } + return NO; } - NSString* path = [[url path] stringByExpandingTildeInPath]; + NSURL *folderURL = [NSURL fileURLWithPath:path isDirectory:YES]; + NSWorkspace *workspace = [NSWorkspace sharedWorkspace]; + for (NSString *bundleIdentifier in OICPreferredVSCodeBundleIdentifiers()) { + NSURL *applicationURL = [workspace URLForApplicationWithBundleIdentifier:bundleIdentifier]; + if (applicationURL == nil) { + continue; + } + + NSWorkspaceOpenConfiguration *configuration = [NSWorkspaceOpenConfiguration configuration]; + [configuration setActivates:YES]; + [configuration setPromptsUserIfNeeded:YES]; + [configuration setAllowsRunningApplicationSubstitution:NO]; + + dispatch_semaphore_t completion = dispatch_semaphore_create(0); + __block BOOL opened = NO; + __block NSString *attemptError = nil; - BOOL isDir = NO; - [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]; + [workspace openURLs:@[folderURL] + withApplicationAtURL:applicationURL + configuration:configuration + completionHandler:^(NSRunningApplication *application, NSError *error) { + opened = application != nil && error == nil; + if (error != nil) { + attemptError = [[error localizedDescription] copy]; + } + dispatch_semaphore_signal(completion); + }]; - if(!isDir){ - path = [path stringByDeletingLastPathComponent]; - } + dispatch_semaphore_wait(completion, DISPATCH_TIME_FOREVER); +#if !OS_OBJECT_USE_OBJC + dispatch_release(completion); +#endif - return path; + if (opened) { + [attemptError release]; + return YES; + } + + if (errorMessage != NULL) { + if ([attemptError length] > 0) { + *errorMessage = [attemptError autorelease]; + attemptError = nil; + } else { + *errorMessage = @"Visual Studio Code could not open the selected folder."; + } + } + + [attemptError release]; + return NO; + } + + if (errorMessage != NULL) { + *errorMessage = @"Install Visual Studio Code or Visual Studio Code Insiders, then try again."; + } + return NO; } int main(int argc, char *argv[]) { - id pool = [[NSAutoreleasePool alloc] init]; - - NSString* path; - @try{ - path = getPathToFrontFinderWindow(); - }@catch(id ex){ - path =[@"~/Desktop" stringByExpandingTildeInPath]; - } - - [[NSTask launchedTaskWithLaunchPath:@"/usr/bin/open" arguments:@[@"-n", @"-b" ,@"com.microsoft.VSCode", @"--args", path]] waitUntilExit]; - - [pool release]; - return 0; + @autoreleasepool { + NSString *finderError = nil; + NSString *path = pathToFrontFinderLocation(&finderError); + + if ([path length] == 0) { + showErrorAlert(@"Couldn’t read the Finder location", finderError ?: @"Open a Finder window and try again."); + return 1; + } + + NSString *launchError = nil; + if (!openPathInPreferredVSCode(path, &launchError)) { + showErrorAlert(@"Couldn’t open Visual Studio Code", launchError ?: @"The application could not be launched."); + return 2; + } + + return 0; + } } diff --git a/scripts/render-homebrew-cask.sh b/scripts/render-homebrew-cask.sh new file mode 100755 index 0000000..cf95341 --- /dev/null +++ b/scripts/render-homebrew-cask.sh @@ -0,0 +1,46 @@ +#!/bin/bash +set -euo pipefail + +if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then + echo "Usage: $0 [output-file]" >&2 + exit 2 +fi + +version="$1" +sha256="$2" +output="${3:-/dev/stdout}" + +if [[ ! "$version" =~ ^[0-9]+([.][0-9]+)*([.-][0-9A-Za-z.-]+)?$ ]]; then + echo "Invalid version: $version" >&2 + exit 2 +fi + +if [[ ! "$sha256" =~ ^[0-9a-f]{64}$ ]]; then + echo "Invalid SHA-256: $sha256" >&2 + exit 2 +fi + +if [ "$output" != "/dev/stdout" ]; then + mkdir -p "$(dirname "$output")" +fi + +cat > "$output" </dev/null +grep -q 'version "1.2.3"' "$cask_path" +grep -q 'app "Open in Code.app"' "$cask_path" + +"$build_dir/OpenInCodeCoreTests" diff --git a/vscode.icon/Assets/vscode-alt.png b/vscode.icon/Assets/vscode-alt.png new file mode 100644 index 0000000..168d84a Binary files /dev/null and b/vscode.icon/Assets/vscode-alt.png differ diff --git a/vscode.icon/icon.json b/vscode.icon/icon.json new file mode 100644 index 0000000..5616bd6 --- /dev/null +++ b/vscode.icon/icon.json @@ -0,0 +1,38 @@ +{ + "fill" : { + "automatic-gradient" : "extended-srgb:0.00000,0.47843,0.80000,1.00000" + }, + "groups" : [ + { + "layers" : [ + { + "blend-mode" : "normal", + "glass" : true, + "hidden" : false, + "image-name" : "vscode-alt.png", + "name" : "vscode-alt", + "position" : { + "scale" : 0.8, + "translation-in-points" : [ + 0, + 0 + ] + } + } + ], + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : true, + "value" : 0.5 + } + } + ], + "supported-platforms" : { + "squares" : [ + "macOS" + ] + } +}