├── .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 |
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 |
824 |
825 |
826 |
827 |
828 |
829 |
830 |
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])