├── .github └── workflows │ ├── release.yml │ └── tag.yml ├── .gitignore ├── Default Browser.entitlements ├── DefaultBrowser.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ └── IconGenerator.xcscheme ├── DefaultBrowser ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── DefaultBrowserPlain128@1x.png │ │ ├── DefaultBrowserPlain128@2x.png │ │ ├── DefaultBrowserPlain16@1x.png │ │ ├── DefaultBrowserPlain16@2x.png │ │ ├── DefaultBrowserPlain256@1x.png │ │ ├── DefaultBrowserPlain256@2x.png │ │ ├── DefaultBrowserPlain32@1x.png │ │ ├── DefaultBrowserPlain32@2x.png │ │ ├── DefaultBrowserPlain512@1x.png │ │ └── DefaultBrowserPlain512@2x.png │ ├── Contents.json │ ├── StatusBarButtonImage.imageset │ │ ├── Contents.json │ │ ├── DefaultBrowser@1x.png │ │ └── DefaultBrowser@2x.png │ └── StatusBarButtonImageError.imageset │ │ ├── Contents.json │ │ ├── DefaultBrowserError@1x.png │ │ └── DefaultBrowserError@2x.png ├── Base.lproj │ └── MainMenu.xib ├── Bundle.swift ├── Defaults.swift ├── ImageTransforms.swift ├── Info.plist ├── Intents.intentdefinition ├── Intents.swift └── SystemUtilities.swift ├── IconGenerator └── main.swift ├── README.md └── test.html /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release management 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: apexskier/github-release-commenter@v1 13 | with: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | comment-template: This will be shipped in version {release_link}. 16 | -------------------------------------------------------------------------------- /.github/workflows/tag.yml: -------------------------------------------------------------------------------- 1 | name: Tag creation 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Get the version 14 | id: version 15 | run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} 16 | shell: bash 17 | 18 | - uses: apexskier/github-semver-parse@v1 19 | id: semver 20 | with: 21 | version: ${{ steps.version.outputs.VERSION }} 22 | 23 | - name: Release 24 | if: ${{ steps.semver.outputs.version }} 25 | uses: softprops/action-gh-release@v1 26 | with: 27 | tag_name: ${{ steps.version.outputs.VERSION }} 28 | prerelease: ${{ !!steps.semver.outputs.prerelease }} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata 19 | 20 | ## Other 21 | *.xccheckout 22 | *.moved-aside 23 | *.xcuserstate 24 | *.xcscmblueprint 25 | *.xcworkspace 26 | !default.xcworkspace 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | *.ipa 31 | 32 | # CocoaPods 33 | # 34 | # We recommend against adding the Pods directory to your .gitignore. However 35 | # you should judge for yourself, the pros and cons are mentioned at: 36 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 37 | # 38 | # Pods/ 39 | 40 | # Carthage 41 | # 42 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 43 | # Carthage/Checkouts 44 | 45 | Carthage/Build 46 | 47 | # fastlane 48 | # 49 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 50 | # screenshots whenever they are needed. 51 | 52 | fastlane/report.xml 53 | fastlane/screenshots 54 | 55 | # used when switching from/to gh-pages 56 | /node_modules 57 | /icons 58 | -------------------------------------------------------------------------------- /Default Browser.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DefaultBrowser.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 70; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | C57375C52DCBC4B20010D944 /* test.html in Resources */ = {isa = PBXBuildFile; fileRef = C5AEAF5D29292C0A00F57C2F /* test.html */; }; 11 | /* End PBXBuildFile section */ 12 | 13 | /* Begin PBXCopyFilesBuildPhase section */ 14 | C56C6D692DCD272000CCAC17 /* CopyFiles */ = { 15 | isa = PBXCopyFilesBuildPhase; 16 | buildActionMask = 2147483647; 17 | dstPath = /usr/share/man/man1/; 18 | dstSubfolderSpec = 0; 19 | files = ( 20 | ); 21 | runOnlyForDeploymentPostprocessing = 1; 22 | }; 23 | C5AF62F1292F095F0087D317 /* Embed ExtensionKit Extensions */ = { 24 | isa = PBXCopyFilesBuildPhase; 25 | buildActionMask = 2147483647; 26 | dstPath = "$(EXTENSIONS_FOLDER_PATH)"; 27 | dstSubfolderSpec = 16; 28 | files = ( 29 | ); 30 | name = "Embed ExtensionKit Extensions"; 31 | runOnlyForDeploymentPostprocessing = 0; 32 | }; 33 | /* End PBXCopyFilesBuildPhase section */ 34 | 35 | /* Begin PBXFileReference section */ 36 | C56C6D6B2DCD272000CCAC17 /* IconGenerator */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = IconGenerator; sourceTree = BUILT_PRODUCTS_DIR; }; 37 | C5A333731BDAF6EA000767B2 /* Default Browser.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Default Browser.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 38 | C5AEAF5D29292C0A00F57C2F /* test.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = test.html; sourceTree = ""; }; 39 | C5AEAF5E29292C2900F57C2F /* .github */ = {isa = PBXFileReference; lastKnownFileType = folder; path = .github; sourceTree = ""; }; 40 | C5C938A0249BF1AD0008EF81 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 41 | C5E39DC31BE893EA004E722B /* Default Browser.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = "Default Browser.entitlements"; sourceTree = ""; }; 42 | /* End PBXFileReference section */ 43 | 44 | /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ 45 | C56C6D8E2DCD28F200CCAC17 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { 46 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet; 47 | membershipExceptions = ( 48 | Info.plist, 49 | ); 50 | target = C5A333721BDAF6EA000767B2 /* Default Browser */; 51 | }; 52 | C56C6D8F2DCD28F200CCAC17 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { 53 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet; 54 | membershipExceptions = ( 55 | Bundle.swift, 56 | ImageTransforms.swift, 57 | SystemUtilities.swift, 58 | ); 59 | target = C56C6D6A2DCD272000CCAC17 /* IconGenerator */; 60 | }; 61 | /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ 62 | 63 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 64 | C56C6D6C2DCD272000CCAC17 /* IconGenerator */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = IconGenerator; sourceTree = ""; }; 65 | C56C6D802DCD28F200CCAC17 /* DefaultBrowser */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (C56C6D8E2DCD28F200CCAC17 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, C56C6D8F2DCD28F200CCAC17 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = DefaultBrowser; sourceTree = ""; }; 66 | /* End PBXFileSystemSynchronizedRootGroup section */ 67 | 68 | /* Begin PBXFrameworksBuildPhase section */ 69 | C56C6D682DCD272000CCAC17 /* Frameworks */ = { 70 | isa = PBXFrameworksBuildPhase; 71 | buildActionMask = 2147483647; 72 | files = ( 73 | ); 74 | runOnlyForDeploymentPostprocessing = 0; 75 | }; 76 | C5A333701BDAF6EA000767B2 /* Frameworks */ = { 77 | isa = PBXFrameworksBuildPhase; 78 | buildActionMask = 2147483647; 79 | files = ( 80 | ); 81 | runOnlyForDeploymentPostprocessing = 0; 82 | }; 83 | /* End PBXFrameworksBuildPhase section */ 84 | 85 | /* Begin PBXGroup section */ 86 | C5A3336A1BDAF6EA000767B2 = { 87 | isa = PBXGroup; 88 | children = ( 89 | C5AEAF5E29292C2900F57C2F /* .github */, 90 | C5AEAF5D29292C0A00F57C2F /* test.html */, 91 | C5C938A0249BF1AD0008EF81 /* README.md */, 92 | C5E39DC31BE893EA004E722B /* Default Browser.entitlements */, 93 | C56C6D802DCD28F200CCAC17 /* DefaultBrowser */, 94 | C56C6D6C2DCD272000CCAC17 /* IconGenerator */, 95 | C5A333741BDAF6EA000767B2 /* Products */, 96 | ); 97 | sourceTree = ""; 98 | }; 99 | C5A333741BDAF6EA000767B2 /* Products */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | C5A333731BDAF6EA000767B2 /* Default Browser.app */, 103 | C56C6D6B2DCD272000CCAC17 /* IconGenerator */, 104 | ); 105 | name = Products; 106 | sourceTree = ""; 107 | }; 108 | /* End PBXGroup section */ 109 | 110 | /* Begin PBXNativeTarget section */ 111 | C56C6D6A2DCD272000CCAC17 /* IconGenerator */ = { 112 | isa = PBXNativeTarget; 113 | buildConfigurationList = C56C6D712DCD272000CCAC17 /* Build configuration list for PBXNativeTarget "IconGenerator" */; 114 | buildPhases = ( 115 | C56C6D672DCD272000CCAC17 /* Sources */, 116 | C56C6D682DCD272000CCAC17 /* Frameworks */, 117 | C56C6D692DCD272000CCAC17 /* CopyFiles */, 118 | ); 119 | buildRules = ( 120 | ); 121 | dependencies = ( 122 | ); 123 | fileSystemSynchronizedGroups = ( 124 | C56C6D6C2DCD272000CCAC17 /* IconGenerator */, 125 | ); 126 | name = IconGenerator; 127 | packageProductDependencies = ( 128 | ); 129 | productName = IconGenerator; 130 | productReference = C56C6D6B2DCD272000CCAC17 /* IconGenerator */; 131 | productType = "com.apple.product-type.tool"; 132 | }; 133 | C5A333721BDAF6EA000767B2 /* Default Browser */ = { 134 | isa = PBXNativeTarget; 135 | buildConfigurationList = C5A333961BDAF6EA000767B2 /* Build configuration list for PBXNativeTarget "Default Browser" */; 136 | buildPhases = ( 137 | C5A3336F1BDAF6EA000767B2 /* Sources */, 138 | C5A333711BDAF6EA000767B2 /* Resources */, 139 | C5A333701BDAF6EA000767B2 /* Frameworks */, 140 | C5AF62F1292F095F0087D317 /* Embed ExtensionKit Extensions */, 141 | ); 142 | buildRules = ( 143 | ); 144 | dependencies = ( 145 | ); 146 | fileSystemSynchronizedGroups = ( 147 | C56C6D802DCD28F200CCAC17 /* DefaultBrowser */, 148 | ); 149 | name = "Default Browser"; 150 | productName = DefaultBrowser; 151 | productReference = C5A333731BDAF6EA000767B2 /* Default Browser.app */; 152 | productType = "com.apple.product-type.application"; 153 | }; 154 | /* End PBXNativeTarget section */ 155 | 156 | /* Begin PBXProject section */ 157 | C5A3336B1BDAF6EA000767B2 /* Project object */ = { 158 | isa = PBXProject; 159 | attributes = { 160 | BuildIndependentTargetsInParallel = YES; 161 | LastSwiftUpdateCheck = 1620; 162 | LastUpgradeCheck = 1630; 163 | ORGANIZATIONNAME = "Cameron Little"; 164 | TargetAttributes = { 165 | C56C6D6A2DCD272000CCAC17 = { 166 | CreatedOnToolsVersion = 16.2; 167 | }; 168 | C5A333721BDAF6EA000767B2 = { 169 | CreatedOnToolsVersion = 7.1; 170 | DevelopmentTeam = 6D9DLTQJSN; 171 | LastSwiftMigration = 1030; 172 | SystemCapabilities = { 173 | com.apple.Sandbox = { 174 | enabled = 1; 175 | }; 176 | }; 177 | }; 178 | }; 179 | }; 180 | buildConfigurationList = C5A3336E1BDAF6EA000767B2 /* Build configuration list for PBXProject "DefaultBrowser" */; 181 | compatibilityVersion = "Xcode 3.2"; 182 | developmentRegion = en; 183 | hasScannedForEncodings = 0; 184 | knownRegions = ( 185 | en, 186 | Base, 187 | ); 188 | mainGroup = C5A3336A1BDAF6EA000767B2; 189 | productRefGroup = C5A333741BDAF6EA000767B2 /* Products */; 190 | projectDirPath = ""; 191 | projectRoot = ""; 192 | targets = ( 193 | C5A333721BDAF6EA000767B2 /* Default Browser */, 194 | C56C6D6A2DCD272000CCAC17 /* IconGenerator */, 195 | ); 196 | }; 197 | /* End PBXProject section */ 198 | 199 | /* Begin PBXResourcesBuildPhase section */ 200 | C5A333711BDAF6EA000767B2 /* Resources */ = { 201 | isa = PBXResourcesBuildPhase; 202 | buildActionMask = 2147483647; 203 | files = ( 204 | C57375C52DCBC4B20010D944 /* test.html in Resources */, 205 | ); 206 | runOnlyForDeploymentPostprocessing = 0; 207 | }; 208 | /* End PBXResourcesBuildPhase section */ 209 | 210 | /* Begin PBXSourcesBuildPhase section */ 211 | C56C6D672DCD272000CCAC17 /* Sources */ = { 212 | isa = PBXSourcesBuildPhase; 213 | buildActionMask = 2147483647; 214 | files = ( 215 | ); 216 | runOnlyForDeploymentPostprocessing = 0; 217 | }; 218 | C5A3336F1BDAF6EA000767B2 /* Sources */ = { 219 | isa = PBXSourcesBuildPhase; 220 | buildActionMask = 2147483647; 221 | files = ( 222 | ); 223 | runOnlyForDeploymentPostprocessing = 0; 224 | }; 225 | /* End PBXSourcesBuildPhase section */ 226 | 227 | /* Begin XCBuildConfiguration section */ 228 | C56C6D6F2DCD272000CCAC17 /* Debug */ = { 229 | isa = XCBuildConfiguration; 230 | buildSettings = { 231 | CLANG_ANALYZER_NONNULL = YES; 232 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 233 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 234 | CLANG_ENABLE_OBJC_WEAK = YES; 235 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 236 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 237 | CODE_SIGN_STYLE = Automatic; 238 | ENABLE_HARDENED_RUNTIME = YES; 239 | GCC_C_LANGUAGE_STANDARD = gnu17; 240 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 241 | MACOSX_DEPLOYMENT_TARGET = 15.2; 242 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 243 | MTL_FAST_MATH = YES; 244 | PRODUCT_NAME = "$(TARGET_NAME)"; 245 | SDKROOT = macosx; 246 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 247 | SWIFT_VERSION = 5.0; 248 | }; 249 | name = Debug; 250 | }; 251 | C56C6D702DCD272000CCAC17 /* Release */ = { 252 | isa = XCBuildConfiguration; 253 | buildSettings = { 254 | CLANG_ANALYZER_NONNULL = YES; 255 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 256 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 257 | CLANG_ENABLE_OBJC_WEAK = YES; 258 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 259 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 260 | CODE_SIGN_STYLE = Automatic; 261 | ENABLE_HARDENED_RUNTIME = YES; 262 | GCC_C_LANGUAGE_STANDARD = gnu17; 263 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 264 | MACOSX_DEPLOYMENT_TARGET = 15.2; 265 | MTL_FAST_MATH = YES; 266 | PRODUCT_NAME = "$(TARGET_NAME)"; 267 | SDKROOT = macosx; 268 | SWIFT_VERSION = 5.0; 269 | }; 270 | name = Release; 271 | }; 272 | C5A333941BDAF6EA000767B2 /* Debug */ = { 273 | isa = XCBuildConfiguration; 274 | buildSettings = { 275 | ALWAYS_SEARCH_USER_PATHS = NO; 276 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 277 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 278 | CLANG_ENABLE_MODULES = YES; 279 | CLANG_ENABLE_OBJC_ARC = YES; 280 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 281 | CLANG_WARN_BOOL_CONVERSION = YES; 282 | CLANG_WARN_COMMA = YES; 283 | CLANG_WARN_CONSTANT_CONVERSION = YES; 284 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 285 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 286 | CLANG_WARN_EMPTY_BODY = YES; 287 | CLANG_WARN_ENUM_CONVERSION = YES; 288 | CLANG_WARN_INFINITE_RECURSION = YES; 289 | CLANG_WARN_INT_CONVERSION = YES; 290 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 291 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 292 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 293 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 294 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 295 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 296 | CLANG_WARN_STRICT_PROTOTYPES = YES; 297 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 298 | CLANG_WARN_UNREACHABLE_CODE = YES; 299 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 300 | CODE_SIGN_IDENTITY = "-"; 301 | COPY_PHASE_STRIP = NO; 302 | DEAD_CODE_STRIPPING = YES; 303 | DEBUG_INFORMATION_FORMAT = dwarf; 304 | DEVELOPMENT_TEAM = 6D9DLTQJSN; 305 | ENABLE_STRICT_OBJC_MSGSEND = YES; 306 | ENABLE_TESTABILITY = YES; 307 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 308 | GCC_C_LANGUAGE_STANDARD = gnu99; 309 | GCC_DYNAMIC_NO_PIC = NO; 310 | GCC_NO_COMMON_BLOCKS = YES; 311 | GCC_OPTIMIZATION_LEVEL = 0; 312 | GCC_PREPROCESSOR_DEFINITIONS = ( 313 | "DEBUG=1", 314 | "$(inherited)", 315 | ); 316 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 317 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 318 | GCC_WARN_UNDECLARED_SELECTOR = YES; 319 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 320 | GCC_WARN_UNUSED_FUNCTION = YES; 321 | GCC_WARN_UNUSED_VARIABLE = YES; 322 | MACOSX_DEPLOYMENT_TARGET = 10.11; 323 | MTL_ENABLE_DEBUG_INFO = YES; 324 | ONLY_ACTIVE_ARCH = YES; 325 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 326 | VERSIONING_SYSTEM = "apple-generic"; 327 | VERSION_INFO_SUFFIX = _debug; 328 | }; 329 | name = Debug; 330 | }; 331 | C5A333951BDAF6EA000767B2 /* Release */ = { 332 | isa = XCBuildConfiguration; 333 | buildSettings = { 334 | ALWAYS_SEARCH_USER_PATHS = NO; 335 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 336 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 337 | CLANG_ENABLE_MODULES = YES; 338 | CLANG_ENABLE_OBJC_ARC = YES; 339 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 340 | CLANG_WARN_BOOL_CONVERSION = YES; 341 | CLANG_WARN_COMMA = YES; 342 | CLANG_WARN_CONSTANT_CONVERSION = YES; 343 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 344 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 345 | CLANG_WARN_EMPTY_BODY = YES; 346 | CLANG_WARN_ENUM_CONVERSION = YES; 347 | CLANG_WARN_INFINITE_RECURSION = YES; 348 | CLANG_WARN_INT_CONVERSION = YES; 349 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 350 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 351 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 352 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 353 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 354 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 355 | CLANG_WARN_STRICT_PROTOTYPES = YES; 356 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 357 | CLANG_WARN_UNREACHABLE_CODE = YES; 358 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 359 | CODE_SIGN_IDENTITY = "-"; 360 | COPY_PHASE_STRIP = NO; 361 | DEAD_CODE_STRIPPING = YES; 362 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 363 | DEVELOPMENT_TEAM = 6D9DLTQJSN; 364 | ENABLE_NS_ASSERTIONS = NO; 365 | ENABLE_STRICT_OBJC_MSGSEND = YES; 366 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 367 | GCC_C_LANGUAGE_STANDARD = gnu99; 368 | GCC_NO_COMMON_BLOCKS = YES; 369 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 370 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 371 | GCC_WARN_UNDECLARED_SELECTOR = YES; 372 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 373 | GCC_WARN_UNUSED_FUNCTION = YES; 374 | GCC_WARN_UNUSED_VARIABLE = YES; 375 | MACOSX_DEPLOYMENT_TARGET = 10.11; 376 | MTL_ENABLE_DEBUG_INFO = NO; 377 | SWIFT_COMPILATION_MODE = wholemodule; 378 | VERSIONING_SYSTEM = "apple-generic"; 379 | }; 380 | name = Release; 381 | }; 382 | C5A333971BDAF6EA000767B2 /* Debug */ = { 383 | isa = XCBuildConfiguration; 384 | buildSettings = { 385 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 386 | CODE_SIGN_ENTITLEMENTS = "Default Browser.entitlements"; 387 | CODE_SIGN_IDENTITY = "Mac Developer"; 388 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 389 | COMBINE_HIDPI_IMAGES = YES; 390 | DEAD_CODE_STRIPPING = YES; 391 | ENABLE_HARDENED_RUNTIME = YES; 392 | INFOPLIST_FILE = DefaultBrowser/Info.plist; 393 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 394 | LD_RUNPATH_SEARCH_PATHS = ( 395 | "$(inherited)", 396 | "@executable_path/../Frameworks", 397 | ); 398 | MACOSX_DEPLOYMENT_TARGET = 10.15; 399 | MARKETING_VERSION = 1.5.0; 400 | PRODUCT_BUNDLE_IDENTIFIER = com.camlittle.DefaultBrowser; 401 | PRODUCT_NAME = "$(TARGET_NAME)"; 402 | PROVISIONING_PROFILE = ""; 403 | SWIFT_VERSION = 5.0; 404 | }; 405 | name = Debug; 406 | }; 407 | C5A333981BDAF6EA000767B2 /* Release */ = { 408 | isa = XCBuildConfiguration; 409 | buildSettings = { 410 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 411 | CODE_SIGN_ENTITLEMENTS = "Default Browser.entitlements"; 412 | CODE_SIGN_IDENTITY = "Mac Developer"; 413 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 414 | COMBINE_HIDPI_IMAGES = YES; 415 | DEAD_CODE_STRIPPING = YES; 416 | ENABLE_HARDENED_RUNTIME = YES; 417 | INFOPLIST_FILE = DefaultBrowser/Info.plist; 418 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 419 | LD_RUNPATH_SEARCH_PATHS = ( 420 | "$(inherited)", 421 | "@executable_path/../Frameworks", 422 | ); 423 | MACOSX_DEPLOYMENT_TARGET = 10.15; 424 | MARKETING_VERSION = 1.5.0; 425 | PRODUCT_BUNDLE_IDENTIFIER = com.camlittle.DefaultBrowser; 426 | PRODUCT_NAME = "$(TARGET_NAME)"; 427 | PROVISIONING_PROFILE = ""; 428 | SWIFT_VERSION = 5.0; 429 | }; 430 | name = Release; 431 | }; 432 | /* End XCBuildConfiguration section */ 433 | 434 | /* Begin XCConfigurationList section */ 435 | C56C6D712DCD272000CCAC17 /* Build configuration list for PBXNativeTarget "IconGenerator" */ = { 436 | isa = XCConfigurationList; 437 | buildConfigurations = ( 438 | C56C6D6F2DCD272000CCAC17 /* Debug */, 439 | C56C6D702DCD272000CCAC17 /* Release */, 440 | ); 441 | defaultConfigurationIsVisible = 0; 442 | defaultConfigurationName = Release; 443 | }; 444 | C5A3336E1BDAF6EA000767B2 /* Build configuration list for PBXProject "DefaultBrowser" */ = { 445 | isa = XCConfigurationList; 446 | buildConfigurations = ( 447 | C5A333941BDAF6EA000767B2 /* Debug */, 448 | C5A333951BDAF6EA000767B2 /* Release */, 449 | ); 450 | defaultConfigurationIsVisible = 0; 451 | defaultConfigurationName = Release; 452 | }; 453 | C5A333961BDAF6EA000767B2 /* Build configuration list for PBXNativeTarget "Default Browser" */ = { 454 | isa = XCConfigurationList; 455 | buildConfigurations = ( 456 | C5A333971BDAF6EA000767B2 /* Debug */, 457 | C5A333981BDAF6EA000767B2 /* Release */, 458 | ); 459 | defaultConfigurationIsVisible = 0; 460 | defaultConfigurationName = Release; 461 | }; 462 | /* End XCConfigurationList section */ 463 | }; 464 | rootObject = C5A3336B1BDAF6EA000767B2 /* Project object */; 465 | } 466 | -------------------------------------------------------------------------------- /DefaultBrowser.xcodeproj/xcshareddata/xcschemes/IconGenerator.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 44 | 46 | 52 | 53 | 54 | 55 | 58 | 59 | 62 | 63 | 64 | 65 | 71 | 73 | 79 | 80 | 81 | 82 | 84 | 85 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /DefaultBrowser/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // DefaultBrowser 4 | // 5 | // Created by Cameron Little on 10/23/15. 6 | // Copyright © 2015 Cameron Little. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import CoreServices 11 | import Intents 12 | import ServiceManagement 13 | import UniformTypeIdentifiers 14 | 15 | // Menu item tags used to fetch them without a direct reference 16 | enum MenuItemTag: Int { 17 | case BrowserListTop = 1 18 | case BrowserListBottom 19 | case usePrimary 20 | } 21 | 22 | // Height of each menu item's icon 23 | let MENU_ITEM_HEIGHT: CGFloat = 16 24 | 25 | // Adds a bundle id field to menu items and the browser's icon 26 | // used in menu bar and preferences primary browser picker 27 | class BrowserMenuItem: NSMenuItem { 28 | var height: CGFloat? 29 | var bundleIdentifier: String? { 30 | didSet { 31 | let workspace = NSWorkspace.shared 32 | if let bid = bundleIdentifier, 33 | let url = workspace.urlForApplication(withBundleIdentifier: bid) { 34 | image = workspace.icon(forFile: url.relativePath) 35 | if let height { 36 | image?.size = NSSize(width: height, height: height) 37 | } 38 | } 39 | } 40 | } 41 | } 42 | 43 | class MenuBarIconMenuItem: NSMenuItem { 44 | var template: Bool? 45 | var style: MenuBarIconStyle? 46 | } 47 | 48 | @NSApplicationMain 49 | class AppDelegate: NSObject { 50 | @IBOutlet weak var preferencesWindow: NSWindow! 51 | @IBOutlet weak var descriptiveAppNamesCheckbox: NSButton! 52 | @IBOutlet weak var disclosureTriangle: NSButton! 53 | @IBOutlet weak var menuBarIconPopUp: NSPopUpButton! 54 | @IBOutlet weak var browsersPopUp: NSPopUpButton! 55 | @IBOutlet weak var showWindowCheckbox: NSButton! 56 | @IBOutlet weak var blocklistTable: NSTableView! 57 | @IBOutlet weak var blocklistView: NSScrollView! 58 | @IBOutlet weak var blocklistHeightConstraint: NSLayoutConstraint! 59 | @IBOutlet weak var setDefaultButton: NSButton! 60 | 61 | @IBOutlet weak var aboutWindow: NSWindow! 62 | @IBOutlet weak var logo: NSImageView! 63 | @IBOutlet weak var versionString: NSTextField! 64 | @IBOutlet weak var builtByString: NSTextField! 65 | @IBOutlet weak var githubString: NSTextField! 66 | 67 | let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) 68 | let workspace = NSWorkspace.shared 69 | 70 | // a list of all valid browsers installed 71 | var validBrowsers = getAllBrowsers() 72 | 73 | // keep an ordered list of running browsers 74 | var runningBrowsers: [NSRunningApplication] = [] 75 | 76 | var runningBrowsersNotBlocked: [NSRunningApplication] { 77 | runningBrowsers.filter({ runningBrowser in 78 | !defaults.browserBlocklist.contains(where: { blockedBrowser in 79 | runningBrowser.bundleIdentifier == blockedBrowser 80 | }) 81 | }) 82 | } 83 | 84 | // an explicitly chosen default browser 85 | var explicitBrowser: String? = nil 86 | 87 | // the user's "system" default browser 88 | var usePrimaryBrowser: Bool? = false 89 | 90 | // user settings 91 | let defaults = ThisDefaults() 92 | 93 | // get around a bug in the browser list when this app wasn't set as the default OS browser 94 | var firstTime = false 95 | 96 | var primaryBrowserObserver: NSKeyValueObservation? 97 | var blockedBrowserObserver: NSKeyValueObservation? 98 | 99 | // MARK: Signal/Notification Responses 100 | 101 | // Respond to the user opening a link 102 | @objc func handleGetURLEvent(event: NSAppleEventDescriptor, withReplyEvent replyEvent: NSAppleEventDescriptor) { 103 | // not sure if the format always matches what I expect 104 | if let urlDescriptor = event.atIndex(1), 105 | let urlStr = urlDescriptor.stringValue, 106 | let url = URL(string: urlStr) { 107 | _ = openUrls(urls: [url], additionalEventParamDescriptor: replyEvent) 108 | } else { 109 | let errorAlert = NSAlert() 110 | let appName = FileManager.default.displayName(atPath: Bundle.main.bundlePath) 111 | errorAlert.messageText = "Error" 112 | errorAlert.informativeText = "\(appName) couldn't understand an URL. Please report this error." 113 | errorAlert.alertStyle = .critical 114 | errorAlert.addButton(withTitle: "Okay") 115 | errorAlert.addButton(withTitle: "Report") 116 | switch errorAlert.runModal() { 117 | case NSApplication.ModalResponse.alertSecondButtonReturn: 118 | let titleText = "Failed to open URL" 119 | let bodyText = "\(appName) couldn't handle to some url.\n\nInformation:\n```\n\(event.data.base64EncodedString())\n```".addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)! 120 | 121 | var components = URLComponents() 122 | components.scheme = "https" 123 | components.host = "github.com" 124 | components.path = "apexskier/DefaultBrowser/issues/new" 125 | components.queryItems = [ 126 | URLQueryItem(name: "title", value: titleText), 127 | URLQueryItem(name: "body", value: bodyText) 128 | ] 129 | 130 | workspace.open(components.url!) 131 | default: 132 | break 133 | } 134 | } 135 | } 136 | 137 | // Respond to the user opening or quitting applications 138 | override func observeValue( 139 | forKeyPath keyPath: String?, 140 | of object: Any?, 141 | change: [NSKeyValueChangeKey : Any]?, 142 | context: UnsafeMutableRawPointer? 143 | ) { 144 | guard let change = change else { 145 | return 146 | } 147 | 148 | var apps: [NSRunningApplication]? = nil 149 | 150 | if let rv = change[NSKeyValueChangeKey.kindKey] as? UInt, let kind = NSKeyValueChange(rawValue: rv) { 151 | switch kind { 152 | case .insertion: 153 | // Get the inserted apps (usually only one, but you never know) 154 | apps = change[NSKeyValueChangeKey.newKey] as? [NSRunningApplication] 155 | case .removal: 156 | // Get the removed apps (usually only one, but you never know) 157 | apps = change[NSKeyValueChangeKey.oldKey] as? [NSRunningApplication] 158 | default: 159 | return // nothing to refresh; should never happen, but... 160 | } 161 | } 162 | 163 | updateBrowsers(apps: apps) 164 | } 165 | 166 | // Respond to the user changing applications 167 | @objc func applicationChange(notification: NSNotification) { 168 | if let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication { 169 | runningBrowsers.sort { a, _ in 170 | a.bundleIdentifier == app.bundleIdentifier 171 | } 172 | updateMenuItems() 173 | } 174 | } 175 | 176 | // Respond to the user changing appearance 177 | @objc func appearanceChange(notification: NSNotification) { 178 | updateMenuItems() 179 | updateMenuBarIconPopUp() 180 | } 181 | 182 | func openUrls(urls: [URL], additionalEventParamDescriptor descriptor: NSAppleEventDescriptor?) -> Bool { 183 | guard let theBrowser = getOpeningBrowserId() else { 184 | let noBrowserAlert = NSAlert() 185 | let selfName = getAppName(bundleId: Bundle.main.bundleIdentifier!) 186 | noBrowserAlert.messageText = "No Browsers Found" 187 | noBrowserAlert.informativeText = "\(selfName) couldn't find any other installed browsers to use. Install something!" 188 | noBrowserAlert.alertStyle = .warning 189 | noBrowserAlert.runModal() 190 | return false 191 | } 192 | 193 | guard let browserUrl = workspace.urlForApplication(withBundleIdentifier: theBrowser) else { 194 | let alert = NSAlert() 195 | let selfName = getAppName(bundleId: Bundle.main.bundleIdentifier!) 196 | alert.messageText = "Browser Not Found" 197 | alert.informativeText = "\(selfName) couldn't find \(theBrowser)." 198 | alert.alertStyle = .warning 199 | alert.runModal() 200 | return false 201 | } 202 | 203 | print("opening: \(urls) in \(theBrowser)") 204 | let openConfiguration = NSWorkspace.OpenConfiguration() 205 | workspace.open(urls, withApplicationAt: browserUrl, configuration: openConfiguration) 206 | return true 207 | } 208 | 209 | // MARK: Management Methods 210 | 211 | private func updatePreferencesBrowsersPopup() { 212 | browsersPopUp.removeAllItems() 213 | var selectedPrimaryBrowser: NSMenuItem? = nil 214 | for bid in validBrowsers { 215 | let menuItem = BrowserMenuItem(title: appName(for: bid), action: nil, keyEquivalent: "") 216 | menuItem.height = MENU_ITEM_HEIGHT 217 | menuItem.bundleIdentifier = bid 218 | if defaults.primaryBrowser?.lowercased() == bid.lowercased() { 219 | selectedPrimaryBrowser = menuItem 220 | } 221 | browsersPopUp.menu?.addItem(menuItem) 222 | } 223 | browsersPopUp.select(selectedPrimaryBrowser) 224 | } 225 | 226 | private var menuBarCases = MenuBarIconStyle.allCases.flatMap({ [(true, $0), (false, $0)] }) 227 | 228 | private func updateMenuBarIconPopUp() { 229 | menuBarIconPopUp.removeAllItems() 230 | var selected: MenuBarIconMenuItem? = nil 231 | 232 | guard let base = NSImage(named: "StatusBarButtonImage") else { 233 | return 234 | } 235 | 236 | for style in MenuBarIconStyle.allCases { 237 | for template in [true, false] { 238 | let menuItem = MenuBarIconMenuItem(title: "\(template ? "Adaptive" : "Full Color") \(style.description)", action: nil, keyEquivalent: "") 239 | menuItem.style = style 240 | menuItem.template = template 241 | if defaults.templateMenuBarIcon == template && defaults.menuBarIconStyle == style { 242 | selected = menuItem 243 | } 244 | menuItem.image = generateIcon( 245 | key: IconCacheKey( 246 | appearance: NSApplication.shared.effectiveAppearance, 247 | style: style, 248 | template: template, 249 | size: MENU_ITEM_HEIGHT * 2, 250 | bundleId: defaults.primaryBrowser ?? "com.apple.Safari" 251 | ), 252 | base: base, 253 | in: workspace 254 | ) 255 | menuBarIconPopUp.menu?.addItem(menuItem) 256 | } 257 | } 258 | 259 | menuBarIconPopUp.select(selected) 260 | } 261 | 262 | // update list of currently running browsers 263 | func updateBrowsers(apps: [NSRunningApplication]?) { 264 | if let apps = apps { 265 | /// Use one of the Dictionary extensions to merge the changes into procdict. 266 | for app in apps.filter({ $0.bundleIdentifier != nil }) { 267 | let remove = app.isTerminated // insert or remove? 268 | 269 | if (validBrowsers.contains(app.bundleIdentifier!)) { 270 | if remove { 271 | if let index = runningBrowsers.firstIndex(of: app) { 272 | runningBrowsers.remove(at: index) 273 | } 274 | } else { 275 | runningBrowsers.append(app) 276 | } 277 | } 278 | } 279 | updateMenuItems() 280 | } 281 | } 282 | 283 | // decide which browser should be used to open a link 284 | func getOpeningBrowserId() -> String? { 285 | // if usePrimaryBrowser is true, use that 286 | if let primaryBrowser = defaults.primaryBrowser, usePrimaryBrowser == true { 287 | return primaryBrowser 288 | } 289 | // if an explicit browser is chosen, use that 290 | if let explicitBrowser { 291 | return explicitBrowser 292 | } 293 | // use the last used browser that's running 294 | let blocklist = defaults.browserBlocklist 295 | if let firstRunningBrowser = runningBrowsers 296 | .filter({ runningBrowser in 297 | !blocklist.contains(where: { blockedBrowser in 298 | runningBrowser.bundleIdentifier == blockedBrowser 299 | }) 300 | }) 301 | .first?.bundleIdentifier { 302 | return firstRunningBrowser 303 | } 304 | // if no browsers are running, use the primary one 305 | if let primaryBrowser = defaults.primaryBrowser { 306 | return primaryBrowser 307 | } 308 | // if no primary browser is chosen, pick the first non-blocked one 309 | if let firstAvailableBrowser = validBrowsers.filter({ blocklist.contains($0) }).first { 310 | return firstAvailableBrowser 311 | } 312 | return nil 313 | } 314 | 315 | // check if DefaultBrowser is the OS level link handler 316 | func isCurrentlyDefaultHttpHandler() -> Bool? { 317 | guard let selfBundleID = Bundle.main.bundleIdentifier, 318 | let testUrl = URL(string: "http:"), 319 | let defaultApplicationUrl = workspace.urlForApplication(toOpen: testUrl), 320 | let currentDefaultBrowser = Bundle(url: defaultApplicationUrl)?.bundleIdentifier else { 321 | return nil 322 | } 323 | return currentDefaultBrowser.lowercased() == selfBundleID.lowercased() 324 | } 325 | 326 | // check if DefaultBrowser is the OS level html file handler 327 | func isCurrentlyDefaultHTMLHandler() -> Bool? { 328 | guard let selfBundleID = Bundle.main.bundleIdentifier else { 329 | return nil 330 | } 331 | 332 | if #available(macOS 12.0, *) { 333 | guard let defaultApplicationUrl = workspace.urlForApplication(toOpen: UTType.html), 334 | let currentDefault = Bundle(url: defaultApplicationUrl)?.bundleIdentifier else { 335 | return nil 336 | } 337 | return currentDefault.lowercased() == selfBundleID.lowercased() 338 | } else { 339 | guard let testUrl = Bundle.main.url(forResource: "test", withExtension: "html") else { 340 | return nil 341 | } 342 | var err: Unmanaged? 343 | let applicationUrl = LSCopyDefaultApplicationURLForURL(testUrl as CFURL, .viewer, &err) 344 | if let err { 345 | print(err) 346 | return nil 347 | } 348 | guard let applicationUrl, 349 | let handlerBundleId = Bundle(url: applicationUrl.takeUnretainedValue() as URL)?.bundleIdentifier else { 350 | return nil 351 | } 352 | return handlerBundleId.lowercased() == selfBundleID.lowercased() 353 | } 354 | } 355 | 356 | // set DefaultBrowser as the OS level link handler 357 | func setAsDefaultHttpHandler() { 358 | if #available(macOS 12.0, *) { 359 | if let testUrl = URL(string: "http:"), 360 | let defaultApplicationUrl = workspace.urlForApplication(toOpen: testUrl), 361 | let currentDefaultBrowser = Bundle(url: defaultApplicationUrl)?.bundleIdentifier { 362 | defaults.primaryBrowser = currentDefaultBrowser 363 | } 364 | Task { 365 | do { 366 | try await workspace.setDefaultApplication(at: Bundle.main.bundleURL, toOpenURLsWithScheme: "http") 367 | } catch { 368 | print("failed to set default http scheme handler: \(error)") 369 | let errorAlert = await NSAlert(error: error) 370 | await errorAlert.runModal() 371 | } 372 | await MainActor.run { 373 | updateMenuItems() 374 | } 375 | } 376 | } else { 377 | let selfBundleID = Bundle.main.bundleIdentifier! as CFString 378 | for scheme in browserQualifyingSchemes { 379 | let error = LSSetDefaultHandlerForURLScheme(scheme as CFString, selfBundleID) 380 | if error != noErr { 381 | print("failed to set handler for scheme \(scheme)") 382 | } 383 | } 384 | updateMenuItems() 385 | } 386 | } 387 | 388 | func setAsDefaultHTMLHandler() { 389 | if #available(macOS 12.0, *) { 390 | Task { 391 | do { 392 | try await workspace.setDefaultApplication(at: Bundle.main.bundleURL, toOpen: .html) 393 | } catch { 394 | print("failed to set default html file handler: \(error)") 395 | // this appears to be intentional by Apple, unfortunately 396 | // https://github.com/Hammerspoon/hammerspoon/issues/2205#issuecomment-541972453 397 | } 398 | } 399 | } else { 400 | let selfBundleID = Bundle.main.bundleIdentifier! as CFString 401 | let error = LSSetDefaultRoleHandlerForContentType("public.html" as CFString, .viewer, selfBundleID) 402 | if error != noErr { 403 | print("failed to set html file handler") 404 | } 405 | } 406 | } 407 | 408 | // set to open automatically at login 409 | func setOpenOnLogin() { 410 | if #available(macOS 13.0, *) { 411 | if SMAppService.mainApp.status != .enabled { 412 | try? SMAppService.mainApp.register() 413 | } 414 | } else { 415 | if 416 | let loginItemsRef = LSSharedFileListCreate(nil, kLSSharedFileListSessionLoginItems.takeRetainedValue(), nil)?.takeRetainedValue() as LSSharedFileList?, 417 | let loginItems = LSSharedFileListCopySnapshot(loginItemsRef, nil)?.takeRetainedValue() as? NSArray 418 | { 419 | let appURL = Bundle.main.bundleURL 420 | let lastItemRef = loginItems.lastObject as! LSSharedFileListItem 421 | for currentItem in loginItems { 422 | let currentItemRef: LSSharedFileListItem = currentItem as! LSSharedFileListItem 423 | if let itemURL = LSSharedFileListItemCopyResolvedURL(currentItemRef, 0, nil) { 424 | if (itemURL.takeRetainedValue() as NSURL).isEqual(appURL) { 425 | print("Already registered in startup list.") 426 | return 427 | } 428 | } 429 | } 430 | print("Registering in startup list.") 431 | LSSharedFileListInsertItemURL(loginItemsRef, lastItemRef, nil, nil, appURL as CFURL, nil, nil) 432 | } 433 | } 434 | } 435 | 436 | // reset lists of browsers 437 | func resetBrowsers() { 438 | validBrowsers = getAllBrowsers() 439 | runningBrowsers = [] 440 | updateBrowsers(apps: workspace.runningApplications.sorted { a, _ in 441 | (a.bundleIdentifier ?? "") == defaults.primaryBrowser 442 | }) 443 | updateBlocklistTable() 444 | updatePreferencesBrowsersPopup() 445 | } 446 | 447 | private var iconCache = NSCache() 448 | 449 | func getMenuBarIcon(for bundleId: String) -> NSImage? { 450 | guard let h = NSApplication.shared.mainMenu?.menuBarHeight else { 451 | return nil 452 | } 453 | 454 | let key = IconCacheKey( 455 | appearance: NSApplication.shared.effectiveAppearance, 456 | style: defaults.menuBarIconStyle, 457 | template: defaults.templateMenuBarIcon, 458 | size: h, 459 | bundleId: bundleId 460 | ) 461 | if let image = iconCache.object(forKey: key) { 462 | return image 463 | } 464 | guard let base = NSImage(named: "StatusBarButtonImage") else { 465 | return nil 466 | } 467 | 468 | if let image = generateIcon(key: key, base: base,in: workspace) { 469 | // cache so we don't have to go through all this again 470 | iconCache.setObject(image, forKey: key) 471 | return image 472 | } 473 | 474 | return nil 475 | } 476 | 477 | func appName(for bundleId: String) -> String { 478 | defaults.detailedAppNames 479 | ? getDetailedAppName(bundleId: bundleId) 480 | : getAppName(bundleId: bundleId) 481 | } 482 | 483 | func appName(for app: NSRunningApplication) -> String { 484 | defaults.detailedAppNames 485 | ? getDetailedAppName(bundleId: app.bundleIdentifier ?? "") 486 | : (app.localizedName ?? getAppName(bundleId: app.bundleIdentifier ?? "")) 487 | } 488 | 489 | // refresh menu bar ui 490 | func updateMenuItems() { 491 | guard let menu = statusItem.menu else { 492 | return 493 | } 494 | 495 | let top = menu.indexOfItem(withTag: MenuItemTag.BrowserListTop.rawValue) 496 | let bottom = menu.indexOfItem(withTag: MenuItemTag.BrowserListBottom.rawValue) 497 | for i in ((top+1).. 0 { 823 | disclosureTriangle.state = .on 824 | doDisclosure(sender: disclosureTriangle) 825 | } 826 | 827 | logo.image = NSImage(named: "AppIcon") 828 | 829 | let paragraph = NSMutableParagraphStyle() 830 | paragraph.alignment = .center 831 | let font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize) 832 | 833 | let shortVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") ?? "" 834 | let buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") ?? "" 835 | versionString.attributedStringValue = NSAttributedString( 836 | string: "Version \(shortVersion) (\(buildNumber))", 837 | attributes: [ 838 | .paragraphStyle: paragraph, 839 | .font: font, 840 | ] 841 | ) 842 | 843 | let cameronLink = NSAttributedString( 844 | string: "Cameron Little", 845 | attributes: [ 846 | .link: "https://camlittle.com", 847 | .paragraphStyle: paragraph, 848 | .font: font, 849 | ] 850 | ) 851 | let builtBy = NSMutableAttributedString( 852 | string: "Built by ", 853 | attributes: [ 854 | .paragraphStyle: paragraph, 855 | .font: font, 856 | ] 857 | ) 858 | builtBy.append(cameronLink) 859 | builtByString.allowsEditingTextAttributes = true 860 | builtByString.attributedStringValue = builtBy 861 | 862 | let githubLink = NSAttributedString( 863 | string: "GitHub project", 864 | attributes: [ 865 | .link: "https://github.com/apexskier/DefaultBrowser", 866 | .paragraphStyle: paragraph, 867 | .font: font, 868 | ] 869 | ) 870 | githubString.allowsEditingTextAttributes = true 871 | githubString.attributedStringValue = githubLink 872 | } 873 | 874 | func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { 875 | false 876 | } 877 | 878 | func applicationWillTerminate(aNotification: NSNotification) { 879 | // Insert code here to tear down your application 880 | workspace.removeObserver(self, forKeyPath: "runningApplications") 881 | workspace.notificationCenter.removeObserver(self, name: NSWorkspace.didActivateApplicationNotification, object: nil) 882 | NSAppleEventManager.shared().removeEventHandler(forEventClass: UInt32(kInternetEventClass), andEventID: UInt32(kAEGetURL)) 883 | primaryBrowserObserver?.invalidate() 884 | } 885 | 886 | func application(_ sender: NSApplication, openFile filename: String) -> Bool { 887 | openUrls(urls: [URL(fileURLWithPath: filename)], additionalEventParamDescriptor: nil) 888 | } 889 | 890 | func application(_ sender: NSApplication, openFiles filenames: [String]) { 891 | _ = openUrls(urls: filenames.map({ URL(fileURLWithPath: $0) }), additionalEventParamDescriptor: nil) 892 | } 893 | 894 | @available(macOS 11.0, *) 895 | func application(_ application: NSApplication, handlerFor intent: INIntent) -> Any? { 896 | switch intent { 897 | case is SetCurrentBrowserIntent: 898 | return SetCurrentBrowserIntentHandler() 899 | case is ClearCurrentBrowserIntent: 900 | return ClearCurrentBrowserIntentHandler() 901 | default: 902 | return nil 903 | } 904 | } 905 | } 906 | 907 | extension AppDelegate: NSTableViewDataSource { 908 | } 909 | 910 | extension AppDelegate: NSTableViewDelegate { 911 | func numberOfRows(in tableView: NSTableView) -> Int { 912 | validBrowsers.count 913 | } 914 | 915 | func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { 916 | guard let col = tableColumn else { 917 | return nil 918 | } 919 | 920 | let app = validBrowsers[row] 921 | let cell = tableView.makeView(withIdentifier: col.identifier, owner: self) as! NSTableCellView 922 | if let url = workspace.urlForApplication(withBundleIdentifier: app) { 923 | let image = workspace.icon(forFile: url.relativePath) 924 | image.size = NSSize(width: MENU_ITEM_HEIGHT, height: MENU_ITEM_HEIGHT) 925 | cell.imageView?.image = image 926 | } 927 | cell.textField?.textColor = app == defaults.primaryBrowser 928 | ? .disabledControlTextColor 929 | : .controlTextColor 930 | cell.textField?.stringValue = appName(for: app) 931 | return cell 932 | } 933 | 934 | func tableView(_ tableView: NSTableView, selectionIndexesForProposedSelection proposedSelectionIndexes: IndexSet) -> IndexSet { 935 | defaults.browserBlocklist = proposedSelectionIndexes 936 | .map { validBrowsers[$0] } 937 | .filter { $0 != defaults.primaryBrowser } 938 | if let primaryBrowser = defaults.primaryBrowser, 939 | let primaryIndex = validBrowsers.firstIndex(of: primaryBrowser) { 940 | let newSelection = NSMutableIndexSet(indexSet: proposedSelectionIndexes) 941 | newSelection.remove(primaryIndex) 942 | return newSelection as IndexSet 943 | } 944 | return proposedSelectionIndexes 945 | } 946 | } 947 | -------------------------------------------------------------------------------- /DefaultBrowser/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "DefaultBrowserPlain16@1x.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "DefaultBrowserPlain16@2x.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "DefaultBrowserPlain32@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "DefaultBrowserPlain32@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "DefaultBrowserPlain128@1x.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "DefaultBrowserPlain128@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "DefaultBrowserPlain256@1x.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "DefaultBrowserPlain256@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "DefaultBrowserPlain512@1x.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "DefaultBrowserPlain512@2x.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /DefaultBrowser/Assets.xcassets/AppIcon.appiconset/DefaultBrowserPlain128@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apexskier/DefaultBrowser/5b077f248c261cf4e3209f8c719754e07b0ed870/DefaultBrowser/Assets.xcassets/AppIcon.appiconset/DefaultBrowserPlain128@1x.png -------------------------------------------------------------------------------- /DefaultBrowser/Assets.xcassets/AppIcon.appiconset/DefaultBrowserPlain128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apexskier/DefaultBrowser/5b077f248c261cf4e3209f8c719754e07b0ed870/DefaultBrowser/Assets.xcassets/AppIcon.appiconset/DefaultBrowserPlain128@2x.png -------------------------------------------------------------------------------- /DefaultBrowser/Assets.xcassets/AppIcon.appiconset/DefaultBrowserPlain16@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apexskier/DefaultBrowser/5b077f248c261cf4e3209f8c719754e07b0ed870/DefaultBrowser/Assets.xcassets/AppIcon.appiconset/DefaultBrowserPlain16@1x.png -------------------------------------------------------------------------------- /DefaultBrowser/Assets.xcassets/AppIcon.appiconset/DefaultBrowserPlain16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apexskier/DefaultBrowser/5b077f248c261cf4e3209f8c719754e07b0ed870/DefaultBrowser/Assets.xcassets/AppIcon.appiconset/DefaultBrowserPlain16@2x.png -------------------------------------------------------------------------------- /DefaultBrowser/Assets.xcassets/AppIcon.appiconset/DefaultBrowserPlain256@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apexskier/DefaultBrowser/5b077f248c261cf4e3209f8c719754e07b0ed870/DefaultBrowser/Assets.xcassets/AppIcon.appiconset/DefaultBrowserPlain256@1x.png -------------------------------------------------------------------------------- /DefaultBrowser/Assets.xcassets/AppIcon.appiconset/DefaultBrowserPlain256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apexskier/DefaultBrowser/5b077f248c261cf4e3209f8c719754e07b0ed870/DefaultBrowser/Assets.xcassets/AppIcon.appiconset/DefaultBrowserPlain256@2x.png -------------------------------------------------------------------------------- /DefaultBrowser/Assets.xcassets/AppIcon.appiconset/DefaultBrowserPlain32@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apexskier/DefaultBrowser/5b077f248c261cf4e3209f8c719754e07b0ed870/DefaultBrowser/Assets.xcassets/AppIcon.appiconset/DefaultBrowserPlain32@1x.png -------------------------------------------------------------------------------- /DefaultBrowser/Assets.xcassets/AppIcon.appiconset/DefaultBrowserPlain32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apexskier/DefaultBrowser/5b077f248c261cf4e3209f8c719754e07b0ed870/DefaultBrowser/Assets.xcassets/AppIcon.appiconset/DefaultBrowserPlain32@2x.png -------------------------------------------------------------------------------- /DefaultBrowser/Assets.xcassets/AppIcon.appiconset/DefaultBrowserPlain512@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apexskier/DefaultBrowser/5b077f248c261cf4e3209f8c719754e07b0ed870/DefaultBrowser/Assets.xcassets/AppIcon.appiconset/DefaultBrowserPlain512@1x.png -------------------------------------------------------------------------------- /DefaultBrowser/Assets.xcassets/AppIcon.appiconset/DefaultBrowserPlain512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apexskier/DefaultBrowser/5b077f248c261cf4e3209f8c719754e07b0ed870/DefaultBrowser/Assets.xcassets/AppIcon.appiconset/DefaultBrowserPlain512@2x.png -------------------------------------------------------------------------------- /DefaultBrowser/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /DefaultBrowser/Assets.xcassets/StatusBarButtonImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "filename" : "DefaultBrowser@1x.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "filename" : "DefaultBrowser@2x.png", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "version" : 1, 16 | "author" : "xcode" 17 | }, 18 | "properties" : { 19 | "template-rendering-intent" : "template" 20 | } 21 | } -------------------------------------------------------------------------------- /DefaultBrowser/Assets.xcassets/StatusBarButtonImage.imageset/DefaultBrowser@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apexskier/DefaultBrowser/5b077f248c261cf4e3209f8c719754e07b0ed870/DefaultBrowser/Assets.xcassets/StatusBarButtonImage.imageset/DefaultBrowser@1x.png -------------------------------------------------------------------------------- /DefaultBrowser/Assets.xcassets/StatusBarButtonImage.imageset/DefaultBrowser@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apexskier/DefaultBrowser/5b077f248c261cf4e3209f8c719754e07b0ed870/DefaultBrowser/Assets.xcassets/StatusBarButtonImage.imageset/DefaultBrowser@2x.png -------------------------------------------------------------------------------- /DefaultBrowser/Assets.xcassets/StatusBarButtonImageError.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "DefaultBrowserError@1x.png", 5 | "idiom" : "mac", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "DefaultBrowserError@2x.png", 10 | "idiom" : "mac", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | }, 18 | "properties" : { 19 | "template-rendering-intent" : "template" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DefaultBrowser/Assets.xcassets/StatusBarButtonImageError.imageset/DefaultBrowserError@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apexskier/DefaultBrowser/5b077f248c261cf4e3209f8c719754e07b0ed870/DefaultBrowser/Assets.xcassets/StatusBarButtonImageError.imageset/DefaultBrowserError@1x.png -------------------------------------------------------------------------------- /DefaultBrowser/Assets.xcassets/StatusBarButtonImageError.imageset/DefaultBrowserError@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apexskier/DefaultBrowser/5b077f248c261cf4e3209f8c719754e07b0ed870/DefaultBrowser/Assets.xcassets/StatusBarButtonImageError.imageset/DefaultBrowserError@2x.png -------------------------------------------------------------------------------- /DefaultBrowser/Base.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | Default 555 | 556 | 557 | 558 | 559 | 560 | 561 | Left to Right 562 | 563 | 564 | 565 | 566 | 567 | 568 | Right to Left 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | Default 580 | 581 | 582 | 583 | 584 | 585 | 586 | Left to Right 587 | 588 | 589 | 590 | 591 | 592 | 593 | Right to Left 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 827 | 831 | 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 843 | 844 | 845 | 846 | 847 | 848 | 858 | 868 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | 898 | 899 | 900 | 901 | 902 | 903 | 904 | 905 | 906 | 907 | 908 | 909 | 910 | 911 | 912 | 913 | 914 | 915 | 916 | 917 | 918 | 919 | 920 | 921 | 922 | 923 | 924 | 925 | 926 | 927 | 928 | 929 | 930 | 931 | 932 | 933 | 934 | 935 | 936 | 937 | 938 | 939 | 940 | 941 | 942 | 943 | 944 | 945 | 946 | 947 | 948 | 949 | 950 | 951 | 952 | DefaultBrowser will automatically open links using the last browser you've used. You can disable this temporarily by clicking the menu icon and choosing a browser. 953 | 954 | 955 | 956 | 957 | 958 | 959 | 960 | 961 | 962 | 963 | 964 | 965 | 966 | 967 | 968 | 969 | 970 | 971 | 972 | 973 | 974 | 975 | 976 | 977 | 978 | 979 | 980 | 981 | 982 | 983 | 984 | 985 | 986 | 987 | 988 | 989 | 990 | 991 | 992 | 993 | 994 | 995 | 996 | 997 | 998 | 999 | 1000 | 1001 | 1002 | 1003 | 1004 | 1005 | 1006 | 1007 | 1008 | 1009 | 1010 | 1011 | 1012 | -------------------------------------------------------------------------------- /DefaultBrowser/Bundle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bundle.swift 3 | // Default Browser 4 | // 5 | // Created by Cameron Little on 2022-11-26. 6 | // Copyright © 2022 Cameron Little. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Bundle { 12 | var appName: String? { 13 | let infoDict = (self.localizedInfoDictionary ?? self.infoDictionary) 14 | let localizedName = infoDict?["CFBundleDisplayName"] ?? infoDict?["CFBundleName"] 15 | return localizedName as? String 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /DefaultBrowser/Defaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Defaults.swift 3 | // DefaultBrowser 4 | // 5 | // Created by Cameron Little on 11/2/15. 6 | // Copyright © 2015 Cameron Little. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum MenuBarIconStyle: Int, RawRepresentable, CaseIterable { 12 | case browserIcon = 1 13 | case framed = 2 14 | 15 | var description: String { 16 | switch self { 17 | case .browserIcon: 18 | return "Browser Icon" 19 | case .framed: 20 | return "Framed" 21 | } 22 | } 23 | } 24 | 25 | private enum DefaultKey: String { 26 | case OpenWindowOnLaunch 27 | case DetailedAppNames 28 | case PrimaryBrowser 29 | case BrowserBlocklist 30 | case MenuBarIconStyle 31 | case TemplateMenuBarIcon 32 | 33 | /// @deprecated replaced with BrowserBlocklist 34 | case BrowserBlacklist 35 | } 36 | 37 | // default values for this application's user defaults 38 | // (it's confusing, because the user specific settings are called defaults) 39 | let defaultSettings: [String: AnyObject] = [ 40 | DefaultKey.OpenWindowOnLaunch.rawValue: true as AnyObject, 41 | DefaultKey.DetailedAppNames.rawValue: false as AnyObject, 42 | DefaultKey.PrimaryBrowser.rawValue: "" as AnyObject, 43 | DefaultKey.BrowserBlocklist.rawValue: [] as AnyObject, 44 | DefaultKey.MenuBarIconStyle.rawValue: MenuBarIconStyle.framed.rawValue as AnyObject, 45 | DefaultKey.TemplateMenuBarIcon.rawValue: true as AnyObject 46 | ] 47 | 48 | extension ThisDefaults { 49 | @objc dynamic var PrimaryBrowser: String? { 50 | string(forKey: DefaultKey.PrimaryBrowser.rawValue) 51 | } 52 | 53 | @objc dynamic var BrowserBlocklist: String? { 54 | string(forKey: DefaultKey.BrowserBlocklist.rawValue) 55 | } 56 | } 57 | 58 | class ThisDefaults: UserDefaults { 59 | // Open the preferences window on application launch 60 | var openWindowOnLaunch: Bool { 61 | get { 62 | bool(forKey: DefaultKey.OpenWindowOnLaunch.rawValue) 63 | } 64 | set (value) { 65 | set(value, forKey: DefaultKey.OpenWindowOnLaunch.rawValue) 66 | } 67 | } 68 | 69 | // Show application version in list 70 | var detailedAppNames: Bool { 71 | get { 72 | bool(forKey: DefaultKey.DetailedAppNames.rawValue) 73 | } 74 | set (value) { 75 | set(value, forKey: DefaultKey.DetailedAppNames.rawValue) 76 | } 77 | } 78 | 79 | // The user's primary browser (their old default browser) 80 | var primaryBrowser: String? { 81 | get { 82 | let value = string(forKey: DefaultKey.PrimaryBrowser.rawValue) 83 | if value == "" { 84 | return nil 85 | } 86 | return value 87 | } 88 | set (value) { 89 | // don't set to self 90 | if value != nil && value?.lowercased() == Bundle.main.bundleIdentifier?.lowercased() { 91 | return 92 | } 93 | set(value as? NSString, forKey: DefaultKey.PrimaryBrowser.rawValue) 94 | } 95 | } 96 | 97 | // a list of browsers to never set as default 98 | var browserBlocklist: [String] { 99 | get { 100 | stringArray(forKey: DefaultKey.BrowserBlocklist.rawValue) ?? stringArray(forKey: DefaultKey.BrowserBlacklist.rawValue)! 101 | } 102 | set (value) { 103 | setValue(value, forKey: DefaultKey.BrowserBlocklist.rawValue) 104 | } 105 | } 106 | 107 | var menuBarIconStyle: MenuBarIconStyle { 108 | get { 109 | .init(rawValue: integer(forKey: DefaultKey.MenuBarIconStyle.rawValue)) ?? .framed 110 | } 111 | set (value) { 112 | setValue(value.rawValue, forKey: DefaultKey.MenuBarIconStyle.rawValue) 113 | } 114 | } 115 | 116 | var templateMenuBarIcon: Bool { 117 | get { 118 | bool(forKey: DefaultKey.TemplateMenuBarIcon.rawValue) 119 | } 120 | set (value) { 121 | set(value, forKey: DefaultKey.TemplateMenuBarIcon.rawValue) 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /DefaultBrowser/ImageTransforms.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageTransforms.swift 3 | // Default Browser 4 | // 5 | // Created by Cameron Little on 2025-05-06. 6 | // Copyright © 2025 Cameron Little. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | 11 | extension NSImage { 12 | /// Creates a semi-transparent version of the image 13 | /// - Parameter alpha: The transparency level (0.0 = fully transparent, 1.0 = fully opaque) 14 | /// - Returns: A new NSImage with the specified transparency 15 | func withAlpha(_ alpha: CGFloat) -> NSImage { 16 | guard let cgImage = self.cgImage(forProposedRect: nil, context: nil, hints: nil) else { 17 | return self 18 | } 19 | 20 | return NSImage(size: size, flipped: false) { rect in 21 | guard let context = NSGraphicsContext.current else { 22 | return false 23 | } 24 | 25 | context.imageInterpolation = .high 26 | context.compositingOperation = .copy 27 | 28 | context.cgContext.setAlpha(alpha) 29 | context.cgContext.draw(cgImage, in: rect) 30 | 31 | return true 32 | } 33 | } 34 | } 35 | 36 | // converts a full color image into an inverted template image for use in the menu bar 37 | func convertToTemplateImage(cgImage: CGImage) -> CGImage? { 38 | let width = cgImage.width 39 | let height = cgImage.height 40 | guard let context = CGContext( 41 | data: nil, 42 | width: width, 43 | height: height, 44 | bitsPerComponent: 8, 45 | bytesPerRow: width * 4, 46 | space: CGColorSpaceCreateDeviceRGB(), 47 | bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).rawValue 48 | ) else { 49 | return nil 50 | } 51 | 52 | // Draw original image 53 | context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height)) 54 | 55 | // Get image data 56 | guard let data = context.data else { 57 | return nil 58 | } 59 | 60 | // Process pixels - convert to grayscale and then to black with appropriate transparency 61 | let pixelData = data.bindMemory(to: UInt8.self, capacity: width * height * 4) 62 | for y in 0.. CGImage? { 90 | let width = cgImage.width 91 | let height = cgImage.height 92 | 93 | guard let context = CGContext( 94 | data: nil, 95 | width: width, 96 | height: height, 97 | bitsPerComponent: 8, 98 | bytesPerRow: width * 4, 99 | space: CGColorSpaceCreateDeviceRGB(), 100 | bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).rawValue 101 | ) else { 102 | return nil 103 | } 104 | 105 | // Draw original image 106 | context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height)) 107 | 108 | // Get image data 109 | guard let data = context.data else { 110 | return nil 111 | } 112 | 113 | // Process pixels - convert to grayscale and then to black with appropriate transparency 114 | let pixelData = data.bindMemory(to: UInt8.self, capacity: width * height * 4) 115 | for y in 0.. NSImage? { 164 | guard let iconUrl = workspace.urlForApplication(withBundleIdentifier: key.bundleId), 165 | let baseRep = base.bestRepresentation( 166 | for: NSRect(origin: .zero, size: NSSize(width: key.size, height: key.size)), 167 | context: nil, 168 | hints: [ .interpolation: NSImageInterpolation.high ] 169 | ) 170 | else { 171 | return nil 172 | } 173 | 174 | var rect = CGRect( 175 | origin: .zero, 176 | size: CGSize(width: baseRep.pixelsWide, height: baseRep.pixelsHigh) 177 | ) 178 | guard let baseCG = baseRep.cgImage( 179 | forProposedRect: &rect, 180 | context: nil, 181 | hints: nil 182 | ) else { 183 | return nil 184 | } 185 | 186 | // Create a bitmap context to draw into 187 | guard let context = CGContext( 188 | data: nil, 189 | width: baseRep.pixelsWide, 190 | height: baseRep.pixelsHigh, 191 | bitsPerComponent: 8, 192 | bytesPerRow: 0, 193 | space: CGColorSpaceCreateDeviceRGB(), 194 | bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).rawValue 195 | ) else { 196 | return nil 197 | } 198 | 199 | // calculate the space the browser icon will be drawn into 200 | let browserIconRect: CGRect 201 | if baseRep.pixelsHigh == 32 { 202 | let h = 21.0 203 | browserIconRect = CGRect( 204 | x: 5.5, 205 | y: 32 - h - 9.0, // invert due to flipped coordinate system 206 | width: h, 207 | height: h 208 | ) 209 | } else if baseRep.pixelsHigh == 16 { 210 | let h = 8.0 211 | browserIconRect = CGRect( 212 | x: 4, 213 | y: 16 - h - 7.0, // invert due to flipped coordinate system 214 | width: h, 215 | height: h 216 | ) 217 | } else { 218 | return nil 219 | } 220 | 221 | // fetch browser icon, sized as small as we can to fit the space it'll go into 222 | // if the browser has a simplifed version at small size, it'll look a lot better 223 | guard let browserIconRep = workspace 224 | .icon(forFile: iconUrl.relativePath) 225 | .bestRepresentation( 226 | for: CGRect(origin: .zero, size: browserIconRect.size), 227 | context: nil, 228 | hints: [ .interpolation: NSImageInterpolation.high ] 229 | ), 230 | let browserIconCG = browserIconRep.cgImage(forProposedRect: nil, context: nil, hints: nil) else { 231 | return nil 232 | } 233 | 234 | // invert appropriatly if we're using a template image or not 235 | let baseDrawable: CGImage 236 | let browserDrawable: CGImage 237 | if key.template { 238 | guard let templateBrowserImageCG = convertToTemplateImage(cgImage: browserIconCG) else { 239 | return nil 240 | } 241 | baseDrawable = baseCG 242 | browserDrawable = templateBrowserImageCG 243 | } else { 244 | guard let baseConverted = convertFromTemplateImage(cgImage: baseCG) else { 245 | return nil 246 | } 247 | baseDrawable = baseConverted 248 | browserDrawable = browserIconCG 249 | } 250 | 251 | // assemble the menu bar icon 252 | switch key.style { 253 | case .browserIcon: 254 | context.draw(browserDrawable, in: rect) 255 | case .framed: 256 | context.draw(baseDrawable, in: rect) 257 | context.draw(browserDrawable, in: browserIconRect) 258 | } 259 | 260 | guard let outputCGImage = context.makeImage() else { 261 | return nil 262 | } 263 | 264 | let outputImage = NSImage(cgImage: outputCGImage, size: baseRep.size) 265 | outputImage.isTemplate = key.template 266 | 267 | return outputImage 268 | } 269 | 270 | -------------------------------------------------------------------------------- /DefaultBrowser/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Default Browser 9 | CFBundleDocumentTypes 10 | 11 | 12 | CFBundleTypeName 13 | HTML document 14 | CFBundleTypeRole 15 | Viewer 16 | LSItemContentTypes 17 | 18 | public.html 19 | 20 | LSHandlerRank 21 | Default 22 | 23 | 24 | CFBundleTypeName 25 | XHTML document 26 | CFBundleTypeRole 27 | Viewer 28 | LSItemContentTypes 29 | 30 | public.xhtml 31 | 32 | LSHandlerRank 33 | Default 34 | 35 | 36 | CFBundleExecutable 37 | $(EXECUTABLE_NAME) 38 | CFBundleIdentifier 39 | $(PRODUCT_BUNDLE_IDENTIFIER) 40 | CFBundleInfoDictionaryVersion 41 | 6.0 42 | CFBundleName 43 | $(PRODUCT_NAME) 44 | CFBundlePackageType 45 | APPL 46 | CFBundleShortVersionString 47 | $(MARKETING_VERSION) 48 | CFBundleURLTypes 49 | 50 | 51 | CFBundleURLName 52 | Websites 53 | CFBundleTypeRole 54 | Viewer 55 | CFBundleURLSchemes 56 | 57 | http 58 | https 59 | 60 | 61 | 62 | CFBundleURLName 63 | HTML files 64 | CFBundleTypeRole 65 | Viewer 66 | CFBundleURLSchemes 67 | 68 | file 69 | 70 | 71 | 72 | CFBundleVersion 73 | 294 74 | INIntentsSupported 75 | 76 | SetCurrentBrowserIntent 77 | ClearCurrentBrowserIntent 78 | 79 | ITSAppUsesNonExemptEncryption 80 | 81 | LSApplicationCategoryType 82 | public.app-category.utilities 83 | LSMinimumSystemVersion 84 | $(MACOSX_DEPLOYMENT_TARGET) 85 | LSUIElement 86 | 87 | NSHumanReadableCopyright 88 | Copyright © 2015-2025 Cameron Little. All rights reserved. 89 | NSMainNibFile 90 | MainMenu 91 | NSPrincipalClass 92 | NSApplication 93 | 94 | 95 | -------------------------------------------------------------------------------- /DefaultBrowser/Intents.intentdefinition: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | INEnums 6 | 7 | INIntentDefinitionModelVersion 8 | 1.2 9 | INIntentDefinitionNamespace 10 | wifRKQ 11 | INIntentDefinitionSystemVersion 12 | 21G115 13 | INIntentDefinitionToolsBuildVersion 14 | 14B47b 15 | INIntentDefinitionToolsVersion 16 | 14.1 17 | INIntents 18 | 19 | 20 | INIntentCategory 21 | set 22 | INIntentConfigurable 23 | 24 | INIntentDescription 25 | Temporary set an explicit browser to use, instead of last used. Will reset when Default Browser is quit. 26 | INIntentDescriptionID 27 | 3NNuiV 28 | INIntentInput 29 | browser 30 | INIntentKeyParameter 31 | browser 32 | INIntentLastParameterTag 33 | 1 34 | INIntentManagedParameterCombinations 35 | 36 | browser 37 | 38 | INIntentParameterCombinationSupportsBackgroundExecution 39 | 40 | INIntentParameterCombinationTitle 41 | Override Browser 42 | INIntentParameterCombinationTitleID 43 | jBKOet 44 | INIntentParameterCombinationUpdatesLinked 45 | 46 | 47 | 48 | INIntentName 49 | SetCurrentBrowser 50 | INIntentParameterCombinations 51 | 52 | browser 53 | 54 | INIntentParameterCombinationIsLinked 55 | 56 | INIntentParameterCombinationSupportsBackgroundExecution 57 | 58 | INIntentParameterCombinationTitle 59 | Override Browser 60 | INIntentParameterCombinationTitleID 61 | Khfcbf 62 | 63 | 64 | INIntentParameters 65 | 66 | 67 | INIntentParameterConfigurable 68 | 69 | INIntentParameterCustomDisambiguation 70 | 71 | INIntentParameterDisplayName 72 | Browser 73 | INIntentParameterDisplayNameID 74 | K0rqRe 75 | INIntentParameterDisplayPriority 76 | 1 77 | INIntentParameterMetadata 78 | 79 | INIntentParameterMetadataCapitalization 80 | Sentences 81 | INIntentParameterMetadataDefaultValueID 82 | cpfQIc 83 | 84 | INIntentParameterName 85 | browser 86 | INIntentParameterPromptDialogs 87 | 88 | 89 | INIntentParameterPromptDialogCustom 90 | 91 | INIntentParameterPromptDialogFormatString 92 | There are multiple browsers matching ‘${browser}’. 93 | INIntentParameterPromptDialogFormatStringID 94 | wYyfnK 95 | INIntentParameterPromptDialogType 96 | Configuration 97 | 98 | 99 | INIntentParameterPromptDialogCustom 100 | 101 | INIntentParameterPromptDialogType 102 | Primary 103 | 104 | 105 | INIntentParameterPromptDialogCustom 106 | 107 | INIntentParameterPromptDialogFormatString 108 | There are ${count} browsers matching ‘${browser}’. 109 | INIntentParameterPromptDialogFormatStringID 110 | jsx8lq 111 | INIntentParameterPromptDialogType 112 | DisambiguationIntroduction 113 | 114 | 115 | INIntentParameterPromptDialogCustom 116 | 117 | INIntentParameterPromptDialogFormatString 118 | Just to confirm, you wanted ‘${browser}’? 119 | INIntentParameterPromptDialogFormatStringID 120 | 946kj6 121 | INIntentParameterPromptDialogType 122 | Confirmation 123 | 124 | 125 | INIntentParameterSupportsDynamicEnumeration 126 | 127 | INIntentParameterSupportsResolution 128 | 129 | INIntentParameterTag 130 | 1 131 | INIntentParameterType 132 | String 133 | 134 | 135 | INIntentResponse 136 | 137 | INIntentResponseCodes 138 | 139 | 140 | INIntentResponseCodeName 141 | success 142 | INIntentResponseCodeSuccess 143 | 144 | 145 | 146 | INIntentResponseCodeName 147 | failure 148 | 149 | 150 | 151 | INIntentTitle 152 | Override Browser 153 | INIntentTitleID 154 | GapPKF 155 | INIntentType 156 | Custom 157 | INIntentVerb 158 | Set 159 | 160 | 161 | INIntentCategory 162 | set 163 | INIntentConfigurable 164 | 165 | INIntentDescription 166 | Clear an explicit current browser, and revert to default browser. 167 | INIntentDescriptionID 168 | ALFj2F 169 | INIntentLastParameterTag 170 | 1 171 | INIntentManagedParameterCombinations 172 | 173 | 174 | 175 | INIntentParameterCombinationSupportsBackgroundExecution 176 | 177 | INIntentParameterCombinationTitle 178 | Clear Browser 179 | INIntentParameterCombinationTitleID 180 | 07su0u 181 | INIntentParameterCombinationUpdatesLinked 182 | 183 | 184 | 185 | INIntentName 186 | ClearCurrentBrowser 187 | INIntentParameterCombinations 188 | 189 | 190 | 191 | INIntentParameterCombinationIsLinked 192 | 193 | INIntentParameterCombinationSupportsBackgroundExecution 194 | 195 | INIntentParameterCombinationTitle 196 | Clear Browser 197 | INIntentParameterCombinationTitleID 198 | yPms5B 199 | 200 | 201 | INIntentResponse 202 | 203 | INIntentResponseCodes 204 | 205 | 206 | INIntentResponseCodeName 207 | success 208 | INIntentResponseCodeSuccess 209 | 210 | 211 | 212 | INIntentResponseCodeName 213 | failure 214 | 215 | 216 | 217 | INIntentTitle 218 | Clear Browser 219 | INIntentTitleID 220 | 9VQraf 221 | INIntentType 222 | Custom 223 | INIntentVerb 224 | Set 225 | 226 | 227 | INTypes 228 | 229 | 230 | 231 | -------------------------------------------------------------------------------- /DefaultBrowser/Intents.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Intents.swift 3 | // Default Browser 4 | // 5 | // Created by Cameron Little on 2022-11-23. 6 | // Copyright © 2022 Cameron Little. All rights reserved. 7 | // 8 | 9 | import Intents 10 | import AppKit 11 | 12 | @available(macOS 11.0, *) 13 | class SetCurrentBrowserIntentHandler: NSObject, SetCurrentBrowserIntentHandling { 14 | func handle(intent: SetCurrentBrowserIntent) async -> SetCurrentBrowserIntentResponse { 15 | guard let appDelegate = await NSApplication.shared.delegate as? AppDelegate else { 16 | return SetCurrentBrowserIntentResponse(code: .failureRequiringAppLaunch, userActivity: nil) 17 | } 18 | 19 | guard let browser = intent.browser else { 20 | return SetCurrentBrowserIntentResponse(code: .failure, userActivity: nil) 21 | } 22 | 23 | DispatchQueue.main.sync { 24 | appDelegate.setExplicitBrowser(bundleId: browser) 25 | } 26 | 27 | return SetCurrentBrowserIntentResponse(code: .success, userActivity: nil) 28 | } 29 | 30 | func resolveBrowser(for intent: SetCurrentBrowserIntent) async -> INStringResolutionResult { 31 | guard let inputBrowser = intent.browser else { 32 | return INStringResolutionResult.unsupported() 33 | } 34 | 35 | let browsers = getAllBrowsers() 36 | 37 | if browsers.contains(inputBrowser) { 38 | return INStringResolutionResult.success(with: inputBrowser) 39 | } 40 | 41 | let matchingBrowsers = browsers.filter({ browser in 42 | browser.contains(inputBrowser) 43 | }) 44 | if matchingBrowsers.count == 0 { 45 | return INStringResolutionResult.unsupported() 46 | } 47 | if matchingBrowsers.count == 1 { 48 | return INStringResolutionResult.confirmationRequired(with: matchingBrowsers.first) 49 | } 50 | return INStringResolutionResult.disambiguation(with: matchingBrowsers) 51 | } 52 | 53 | func provideBrowserOptionsCollection(for intent: SetCurrentBrowserIntent) async throws -> INObjectCollection { 54 | let browsers = getAllBrowsers() 55 | return INObjectCollection(items: browsers.map({ NSString(string: $0) })) 56 | } 57 | } 58 | 59 | @available(macOS 11.0, *) 60 | class ClearCurrentBrowserIntentHandler: NSObject, ClearCurrentBrowserIntentHandling { 61 | func handle(intent: ClearCurrentBrowserIntent) async -> ClearCurrentBrowserIntentResponse { 62 | guard let appDelegate = await NSApplication.shared.delegate as? AppDelegate else { 63 | return ClearCurrentBrowserIntentResponse(code: .failureRequiringAppLaunch, userActivity: nil) 64 | } 65 | 66 | DispatchQueue.main.sync { 67 | appDelegate.setExplicitBrowser(bundleId: nil) 68 | } 69 | 70 | return ClearCurrentBrowserIntentResponse(code: .success, userActivity: nil) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /DefaultBrowser/SystemUtilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SystemUtilities.swift 3 | // DefaultBrowser 4 | // 5 | // Created by Cameron Little on 11/4/15. 6 | // Copyright © 2015 Cameron Little. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | let browserQualifyingSchemes = ["https", "http"] 12 | 13 | // return bundle ids for all applications that can open links 14 | func getAllBrowsers() -> [String] { 15 | let browserBids: Set 16 | if #available(macOS 12.0, *) { 17 | let workspace = NSWorkspace.shared 18 | var urlHandlers = Set() 19 | for scheme in browserQualifyingSchemes { 20 | urlHandlers.formUnion(workspace.urlsForApplications(toOpen: URL(string: "\(scheme)://")!)) 21 | } 22 | browserBids = Set(urlHandlers.compactMap({ Bundle(url: $0)?.bundleIdentifier })) 23 | } else { 24 | var handlers = Set() 25 | for scheme in browserQualifyingSchemes { 26 | handlers.formUnion(LSCopyAllHandlersForURLScheme(scheme as CFString)?.takeRetainedValue() as? [String] ?? []) 27 | } 28 | browserBids = handlers 29 | } 30 | 31 | let selfBid = Bundle.main.bundleIdentifier?.lowercased() 32 | return browserBids 33 | .filter({ $0.lowercased() != selfBid }) 34 | .sorted(by: { getAppName(bundleId: $0) < getAppName(bundleId: $1) }) 35 | } 36 | 37 | // return a name for an application's bundle id 38 | func getAppName(bundleId: String) -> String { 39 | if let appUrl = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleId), 40 | let appBundle = Bundle(url: appUrl), 41 | let name = appBundle.appName 42 | ?? appBundle.infoDictionary?["CFBundleExecutable"] as? String 43 | ?? NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleId)?.lastPathComponent { 44 | return name 45 | } 46 | return "Unknown Application" 47 | } 48 | 49 | // return a descriptive name for an application's bundle id 50 | func getDetailedAppName(bundleId: String) -> String { 51 | var name = getAppName(bundleId: bundleId) 52 | if let appUrl = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleId), 53 | let appBundle = Bundle(url: appUrl), 54 | let version = appBundle.infoDictionary?["CFBundleShortVersionString"] as? String { 55 | name += " (\(version))" 56 | } 57 | return name 58 | } 59 | -------------------------------------------------------------------------------- /IconGenerator/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // IconGenerator 4 | // 5 | // Created by Cameron Little on 2025-05-08. 6 | // Copyright © 2025 Cameron Little. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AppKit 11 | 12 | let browsers = getAllBrowsers() 13 | 14 | let workspace = NSWorkspace.shared 15 | 16 | print(CommandLine.arguments) 17 | if CommandLine.arguments.count < 3 { 18 | print("usage: \(CommandLine.arguments[0]) ") 19 | exit(1) 20 | } 21 | 22 | guard let base = NSImage(byReferencingFile: CommandLine.arguments[1]) else { 23 | print("didn't find base image at: \(CommandLine.arguments[1])") 24 | exit(1) 25 | } 26 | 27 | let baseDir = URL(fileURLWithPath: CommandLine.arguments[2]) 28 | 29 | for useTemplate in [true, false] { 30 | for bundleId in browsers { 31 | guard var image = generateIcon( 32 | bundleId, 33 | size: 32, 34 | useTemplate: useTemplate, 35 | base: base, 36 | in: workspace 37 | ) else { 38 | fatalError() 39 | } 40 | 41 | if useTemplate, 42 | let cgimage = image.cgImage(forProposedRect: nil, context: nil, hints: nil), 43 | let reversed = convertFromTemplateImage(cgImage: cgimage) { 44 | image = NSImage(cgImage: reversed, size: image.size) 45 | } 46 | 47 | guard let tiffData = image.tiffRepresentation, 48 | let bitmapImage = NSBitmapImageRep(data: tiffData), 49 | let pngData = bitmapImage.representation(using: .png, properties: [:]) else { 50 | fatalError() 51 | } 52 | 53 | do { 54 | try pngData.write(to: baseDir.appending(path: "DefaultBrowserIcon_\(bundleId)\(useTemplate ? "_template" : "").png")) 55 | } catch { 56 | fatalError(error.localizedDescription) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Default Browser 2 | 3 | Default Browser replaces macOS's system web browser setting with a flexible, convenient utility that opens links with your most recently used browser. 4 | 5 | https://defaultbrowser.app 6 | 7 | ## Features 8 | 9 | - **Intelligent link handling** Opens external links with your last used browser. 10 | - **Quick browser toggle** Keyboard shortcuts from the menu bar. 11 | - **Menu bar preview** Quick reference of the currently active browser. 12 | - **Blocklist** Prevent browsers from automatically opening. 13 | - **Legacy behavior** Select your primary browser to simulate traditional behavior. 14 | - **Shortcuts support** Force a specific browser with Siri Shortcuts 15 | 16 | ## Notes 17 | 18 | - No longer can automatically register as `html` file handler. At some point in the past, Apple restricted the ability of apps to register as the default opener for the UTType `public.html` (https://github.com/Hammerspoon/hammerspoon/issues/2205#issuecomment-541972453). Please [do this manually](https://support.apple.com/guide/mac-help/choose-an-app-to-open-a-file-on-mac-mh35597/mac) now 19 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

It worked?

4 | 5 | 6 | --------------------------------------------------------------------------------