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
-
-
+
+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"
+ ]
+ }
+}