├── .gitignore ├── .gitmodules ├── AppDoesNothing.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings │ └── xcuserdata │ │ └── Koji.xcuserdatad │ │ └── WorkspaceSettings.xcsettings └── xcshareddata │ └── xcschemes │ └── AppDoesNothing.xcscheme ├── LICENSE ├── Main Assets.xcassets ├── Contents.json └── SwiftDefaultApps.appiconset │ ├── Contents.json │ ├── icon_128x128.png │ ├── icon_128x128@2x.png │ ├── icon_16x16.png │ ├── icon_16x16@2x.png │ ├── icon_256x256.png │ ├── icon_256x256@2x.png │ ├── icon_32x32.png │ ├── icon_32x32@2x.png │ ├── icon_512x512.png │ └── icon_512x512@2x.png ├── Package.swift ├── README.md ├── Resources ├── DummyApp_Info.plist ├── Prefpane_Info.plist ├── SwiftCLI_Info.plist └── Unsupported.icns ├── SWDA Prefpane.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings │ └── xcuserdata │ │ └── Koji.xcuserdatad │ │ └── WorkspaceSettings.xcsettings └── xcshareddata │ └── xcschemes │ ├── Build Prefpane.xcscheme │ └── Make Release & Package.xcscheme ├── Sources ├── CLI Components │ ├── getApps.swift │ ├── getHandler.swift │ ├── getSchemes.swift │ ├── getUTIs.swift │ ├── main.swift │ └── setHandler.swift ├── Common Sources │ ├── LSWrappers.swift │ └── commonFuncs.swift ├── DummyApp │ └── AppDelegate.swift └── Prefpane Sources │ ├── Model │ ├── SWDAApplicationInfo.swift │ ├── SWDAContentProtocol.swift │ ├── SWDAHandlersModel.swift │ └── SynchronizedArray.swift │ ├── View Controllers │ ├── ControllersRef.swift │ ├── DryView.swift │ ├── ProgressAlert.swift │ ├── SWDATreeRow.swift │ └── Subclasses.swift │ ├── Views │ ├── SWDAPrefpaneMain.xib │ ├── SWDAPrefpaneTabTemplate.xib │ └── SWDASchemesUTIsContent.xib │ └── main.swift ├── SwiftDefaultApps CLI.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings │ └── xcuserdata │ │ └── Koji.xcuserdatad │ │ └── WorkspaceSettings.xcsettings └── xcshareddata │ └── xcschemes │ ├── Build CLI.xcscheme │ └── xcschememanagement.plist └── SwiftDefaultApps.xcworkspace ├── contents.xcworkspacedata └── xcshareddata ├── IDEWorkspaceChecks.plist ├── SwiftDefaultApps.xcscmblueprint └── WorkspaceSettings.xcsettings /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /SwiftDefaultApps.xcodeproj/xcuserdata/Koji.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist 4 | /SwiftDefaultApps CLI.xcodeproj/xcuserdata 5 | /AppDoesNothing.xcodeproj/xcuserdata 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Packages/SwiftCLI-2.0.3"] 2 | path = Packages/SwiftCLI-2.0.3 3 | url = https://github.com/Lord-Kamina/SwiftCLI 4 | -------------------------------------------------------------------------------- /AppDoesNothing.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | EBA7F0961E856279009EE061 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBA7F0951E856279009EE061 /* AppDelegate.swift */; }; 11 | EBB1976E1E85C5CC0072AF70 /* Unsupported.icns in Resources */ = {isa = PBXBuildFile; fileRef = EBB1976D1E85C5CC0072AF70 /* Unsupported.icns */; }; 12 | /* End PBXBuildFile section */ 13 | 14 | /* Begin PBXFileReference section */ 15 | EBA7F0921E856279009EE061 /* ThisAppDoesNothing.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ThisAppDoesNothing.app; sourceTree = BUILT_PRODUCTS_DIR; }; 16 | EBA7F0951E856279009EE061 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = Sources/DummyApp/AppDelegate.swift; sourceTree = ""; }; 17 | EBA7F09C1E856279009EE061 /* DummyApp_Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = DummyApp_Info.plist; path = Resources/DummyApp_Info.plist; sourceTree = ""; }; 18 | EBB1976D1E85C5CC0072AF70 /* Unsupported.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; name = Unsupported.icns; path = Resources/Unsupported.icns; sourceTree = ""; }; 19 | /* End PBXFileReference section */ 20 | 21 | /* Begin PBXFrameworksBuildPhase section */ 22 | EBA7F08F1E856279009EE061 /* Frameworks */ = { 23 | isa = PBXFrameworksBuildPhase; 24 | buildActionMask = 2147483647; 25 | files = ( 26 | ); 27 | runOnlyForDeploymentPostprocessing = 0; 28 | }; 29 | /* End PBXFrameworksBuildPhase section */ 30 | 31 | /* Begin PBXGroup section */ 32 | EBA7F0891E856279009EE061 = { 33 | isa = PBXGroup; 34 | children = ( 35 | EBB1976D1E85C5CC0072AF70 /* Unsupported.icns */, 36 | EBA7F0951E856279009EE061 /* AppDelegate.swift */, 37 | EBA7F09C1E856279009EE061 /* DummyApp_Info.plist */, 38 | EBA7F0931E856279009EE061 /* Products */, 39 | ); 40 | sourceTree = ""; 41 | }; 42 | EBA7F0931E856279009EE061 /* Products */ = { 43 | isa = PBXGroup; 44 | children = ( 45 | EBA7F0921E856279009EE061 /* ThisAppDoesNothing.app */, 46 | ); 47 | name = Products; 48 | sourceTree = ""; 49 | }; 50 | /* End PBXGroup section */ 51 | 52 | /* Begin PBXNativeTarget section */ 53 | EBA7F0911E856279009EE061 /* ThisAppDoesNothing */ = { 54 | isa = PBXNativeTarget; 55 | buildConfigurationList = EBA7F09F1E856279009EE061 /* Build configuration list for PBXNativeTarget "ThisAppDoesNothing" */; 56 | buildPhases = ( 57 | EBA7F08E1E856279009EE061 /* Sources */, 58 | EBA7F08F1E856279009EE061 /* Frameworks */, 59 | EBA7F0901E856279009EE061 /* Resources */, 60 | ); 61 | buildRules = ( 62 | ); 63 | dependencies = ( 64 | ); 65 | name = ThisAppDoesNothing; 66 | productName = AppDoesNothing; 67 | productReference = EBA7F0921E856279009EE061 /* ThisAppDoesNothing.app */; 68 | productType = "com.apple.product-type.application"; 69 | }; 70 | /* End PBXNativeTarget section */ 71 | 72 | /* Begin PBXProject section */ 73 | EBA7F08A1E856279009EE061 /* Project object */ = { 74 | isa = PBXProject; 75 | attributes = { 76 | LastSwiftUpdateCheck = 0820; 77 | LastUpgradeCheck = 1030; 78 | ORGANIZATIONNAME = "Gregorio Litenstein Goldzweig"; 79 | TargetAttributes = { 80 | EBA7F0911E856279009EE061 = { 81 | CreatedOnToolsVersion = 8.2.1; 82 | LastSwiftMigration = 1020; 83 | }; 84 | }; 85 | }; 86 | buildConfigurationList = EBA7F08D1E856279009EE061 /* Build configuration list for PBXProject "AppDoesNothing" */; 87 | compatibilityVersion = "Xcode 9.3"; 88 | developmentRegion = en; 89 | hasScannedForEncodings = 0; 90 | knownRegions = ( 91 | en, 92 | Base, 93 | ); 94 | mainGroup = EBA7F0891E856279009EE061; 95 | productRefGroup = EBA7F0931E856279009EE061 /* Products */; 96 | projectDirPath = ""; 97 | projectRoot = ""; 98 | targets = ( 99 | EBA7F0911E856279009EE061 /* ThisAppDoesNothing */, 100 | ); 101 | }; 102 | /* End PBXProject section */ 103 | 104 | /* Begin PBXResourcesBuildPhase section */ 105 | EBA7F0901E856279009EE061 /* Resources */ = { 106 | isa = PBXResourcesBuildPhase; 107 | buildActionMask = 2147483647; 108 | files = ( 109 | EBB1976E1E85C5CC0072AF70 /* Unsupported.icns in Resources */, 110 | ); 111 | runOnlyForDeploymentPostprocessing = 0; 112 | }; 113 | /* End PBXResourcesBuildPhase section */ 114 | 115 | /* Begin PBXSourcesBuildPhase section */ 116 | EBA7F08E1E856279009EE061 /* Sources */ = { 117 | isa = PBXSourcesBuildPhase; 118 | buildActionMask = 2147483647; 119 | files = ( 120 | EBA7F0961E856279009EE061 /* AppDelegate.swift in Sources */, 121 | ); 122 | runOnlyForDeploymentPostprocessing = 0; 123 | }; 124 | /* End PBXSourcesBuildPhase section */ 125 | 126 | /* Begin XCBuildConfiguration section */ 127 | EBA7F09D1E856279009EE061 /* Debug */ = { 128 | isa = XCBuildConfiguration; 129 | buildSettings = { 130 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 131 | CLANG_ANALYZER_NONNULL = YES; 132 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 133 | CLANG_ENABLE_MODULES = YES; 134 | CLANG_ENABLE_OBJC_ARC = YES; 135 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 136 | CLANG_WARN_BOOL_CONVERSION = YES; 137 | CLANG_WARN_COMMA = YES; 138 | CLANG_WARN_CONSTANT_CONVERSION = YES; 139 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 140 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 141 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 142 | CLANG_WARN_EMPTY_BODY = YES; 143 | CLANG_WARN_ENUM_CONVERSION = YES; 144 | CLANG_WARN_INFINITE_RECURSION = YES; 145 | CLANG_WARN_INT_CONVERSION = YES; 146 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 147 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 148 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 149 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 150 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 151 | CLANG_WARN_STRICT_PROTOTYPES = YES; 152 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 153 | CLANG_WARN_UNREACHABLE_CODE = YES; 154 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 155 | DEBUG_INFORMATION_FORMAT = dwarf; 156 | ENABLE_STRICT_OBJC_MSGSEND = YES; 157 | ENABLE_TESTABILITY = YES; 158 | GCC_C_LANGUAGE_STANDARD = gnu99; 159 | GCC_NO_COMMON_BLOCKS = YES; 160 | GCC_PREPROCESSOR_DEFINITIONS = ( 161 | "DEBUG=1", 162 | "$(inherited)", 163 | ); 164 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 165 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 166 | GCC_WARN_UNDECLARED_SELECTOR = YES; 167 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 168 | GCC_WARN_UNUSED_FUNCTION = YES; 169 | GCC_WARN_UNUSED_VARIABLE = YES; 170 | LD_RUNPATH_SEARCH_PATHS = ( 171 | "@loader_path/../Frameworks", 172 | "@executable_path/../Frameworks", 173 | ); 174 | MACH_O_TYPE = mh_execute; 175 | MACOSX_DEPLOYMENT_TARGET = 10.12; 176 | MTL_ENABLE_DEBUG_INFO = YES; 177 | ONLY_ACTIVE_ARCH = YES; 178 | SDKROOT = macosx; 179 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 180 | SWIFT_COMPILATION_MODE = singlefile; 181 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 182 | SWIFT_SWIFT3_OBJC_INFERENCE = Off; 183 | SWIFT_VERSION = 5.0; 184 | }; 185 | name = Debug; 186 | }; 187 | EBA7F09E1E856279009EE061 /* Release */ = { 188 | isa = XCBuildConfiguration; 189 | buildSettings = { 190 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 191 | CLANG_ANALYZER_NONNULL = YES; 192 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 193 | CLANG_ENABLE_MODULES = YES; 194 | CLANG_ENABLE_OBJC_ARC = YES; 195 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 196 | CLANG_WARN_BOOL_CONVERSION = YES; 197 | CLANG_WARN_COMMA = YES; 198 | CLANG_WARN_CONSTANT_CONVERSION = YES; 199 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 200 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 201 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 202 | CLANG_WARN_EMPTY_BODY = YES; 203 | CLANG_WARN_ENUM_CONVERSION = YES; 204 | CLANG_WARN_INFINITE_RECURSION = YES; 205 | CLANG_WARN_INT_CONVERSION = YES; 206 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 207 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 208 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 209 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 210 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 211 | CLANG_WARN_STRICT_PROTOTYPES = YES; 212 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 213 | CLANG_WARN_UNREACHABLE_CODE = YES; 214 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 215 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 216 | ENABLE_NS_ASSERTIONS = NO; 217 | ENABLE_STRICT_OBJC_MSGSEND = YES; 218 | GCC_C_LANGUAGE_STANDARD = gnu99; 219 | GCC_NO_COMMON_BLOCKS = YES; 220 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 221 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 222 | GCC_WARN_UNDECLARED_SELECTOR = YES; 223 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 224 | GCC_WARN_UNUSED_FUNCTION = YES; 225 | GCC_WARN_UNUSED_VARIABLE = YES; 226 | LD_RUNPATH_SEARCH_PATHS = ( 227 | "@loader_path/../Frameworks", 228 | "@executable_path/../Frameworks", 229 | ); 230 | MACH_O_TYPE = mh_execute; 231 | MACOSX_DEPLOYMENT_TARGET = 10.12; 232 | MTL_ENABLE_DEBUG_INFO = NO; 233 | SDKROOT = macosx; 234 | SWIFT_COMPILATION_MODE = wholemodule; 235 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 236 | SWIFT_SWIFT3_OBJC_INFERENCE = Off; 237 | SWIFT_VERSION = 5.0; 238 | }; 239 | name = Release; 240 | }; 241 | EBA7F0A01E856279009EE061 /* Debug */ = { 242 | isa = XCBuildConfiguration; 243 | buildSettings = { 244 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 245 | CODE_SIGN_IDENTITY = "Mac Developer"; 246 | CODE_SIGN_STYLE = Automatic; 247 | COMBINE_HIDPI_IMAGES = YES; 248 | DEVELOPMENT_TEAM = 33JDJVTQZT; 249 | INFOPLIST_FILE = "$(SRCROOT)/Resources/DummyApp_Info.plist"; 250 | PRODUCT_BUNDLE_IDENTIFIER = cl.fail.lordkamina.ThisAppDoesNothing; 251 | PRODUCT_NAME = "$(TARGET_NAME)"; 252 | PROVISIONING_PROFILE_SPECIFIER = ""; 253 | SKIP_INSTALL = YES; 254 | }; 255 | name = Debug; 256 | }; 257 | EBA7F0A11E856279009EE061 /* Release */ = { 258 | isa = XCBuildConfiguration; 259 | buildSettings = { 260 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 261 | CODE_SIGN_IDENTITY = "Mac Developer"; 262 | CODE_SIGN_STYLE = Automatic; 263 | COMBINE_HIDPI_IMAGES = YES; 264 | DEVELOPMENT_TEAM = 33JDJVTQZT; 265 | INFOPLIST_FILE = "$(SRCROOT)/Resources/DummyApp_Info.plist"; 266 | PRODUCT_BUNDLE_IDENTIFIER = cl.fail.lordkamina.ThisAppDoesNothing; 267 | PRODUCT_NAME = "$(TARGET_NAME)"; 268 | PROVISIONING_PROFILE_SPECIFIER = ""; 269 | SKIP_INSTALL = YES; 270 | }; 271 | name = Release; 272 | }; 273 | /* End XCBuildConfiguration section */ 274 | 275 | /* Begin XCConfigurationList section */ 276 | EBA7F08D1E856279009EE061 /* Build configuration list for PBXProject "AppDoesNothing" */ = { 277 | isa = XCConfigurationList; 278 | buildConfigurations = ( 279 | EBA7F09D1E856279009EE061 /* Debug */, 280 | EBA7F09E1E856279009EE061 /* Release */, 281 | ); 282 | defaultConfigurationIsVisible = 0; 283 | defaultConfigurationName = Release; 284 | }; 285 | EBA7F09F1E856279009EE061 /* Build configuration list for PBXNativeTarget "ThisAppDoesNothing" */ = { 286 | isa = XCConfigurationList; 287 | buildConfigurations = ( 288 | EBA7F0A01E856279009EE061 /* Debug */, 289 | EBA7F0A11E856279009EE061 /* Release */, 290 | ); 291 | defaultConfigurationIsVisible = 0; 292 | defaultConfigurationName = Release; 293 | }; 294 | /* End XCConfigurationList section */ 295 | }; 296 | rootObject = EBA7F08A1E856279009EE061 /* Project object */; 297 | } 298 | -------------------------------------------------------------------------------- /AppDoesNothing.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /AppDoesNothing.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AppDoesNothing.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildSystemType 6 | Latest 7 | 8 | 9 | -------------------------------------------------------------------------------- /AppDoesNothing.xcodeproj/project.xcworkspace/xcuserdata/Koji.xcuserdatad/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildLocationStyle 6 | UseAppPreferences 7 | BuildSystemType 8 | Latest 9 | CustomBuildLocationType 10 | RelativeToDerivedData 11 | DerivedDataLocationStyle 12 | Default 13 | EnabledFullIndexStoreVisibility 14 | 15 | IssueFilterStyle 16 | ShowActiveSchemeOnly 17 | LiveSourceIssuesEnabled 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /AppDoesNothing.xcodeproj/xcshareddata/xcschemes/AppDoesNothing.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 68 | 69 | 70 | 71 | 72 | 73 | 79 | 81 | 87 | 88 | 89 | 90 | 92 | 93 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return., Gregorio Litenstein. 7 | * ---------------------------------------------------------------------------- 8 | */ -------------------------------------------------------------------------------- /Main Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Main Assets.xcassets/SwiftDefaultApps.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "icon_16x16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "icon_16x16@2x.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "icon_32x32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "icon_32x32@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "icon_128x128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "icon_128x128@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "icon_256x256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "icon_256x256@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "icon_512x512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "icon_512x512@2x.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /Main Assets.xcassets/SwiftDefaultApps.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lord-Kamina/SwiftDefaultApps/4672a7f028f776697ebd25ac640342f3a497e924/Main Assets.xcassets/SwiftDefaultApps.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /Main Assets.xcassets/SwiftDefaultApps.appiconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lord-Kamina/SwiftDefaultApps/4672a7f028f776697ebd25ac640342f3a497e924/Main Assets.xcassets/SwiftDefaultApps.appiconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /Main Assets.xcassets/SwiftDefaultApps.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lord-Kamina/SwiftDefaultApps/4672a7f028f776697ebd25ac640342f3a497e924/Main Assets.xcassets/SwiftDefaultApps.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /Main Assets.xcassets/SwiftDefaultApps.appiconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lord-Kamina/SwiftDefaultApps/4672a7f028f776697ebd25ac640342f3a497e924/Main Assets.xcassets/SwiftDefaultApps.appiconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /Main Assets.xcassets/SwiftDefaultApps.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lord-Kamina/SwiftDefaultApps/4672a7f028f776697ebd25ac640342f3a497e924/Main Assets.xcassets/SwiftDefaultApps.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /Main Assets.xcassets/SwiftDefaultApps.appiconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lord-Kamina/SwiftDefaultApps/4672a7f028f776697ebd25ac640342f3a497e924/Main Assets.xcassets/SwiftDefaultApps.appiconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /Main Assets.xcassets/SwiftDefaultApps.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lord-Kamina/SwiftDefaultApps/4672a7f028f776697ebd25ac640342f3a497e924/Main Assets.xcassets/SwiftDefaultApps.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /Main Assets.xcassets/SwiftDefaultApps.appiconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lord-Kamina/SwiftDefaultApps/4672a7f028f776697ebd25ac640342f3a497e924/Main Assets.xcassets/SwiftDefaultApps.appiconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /Main Assets.xcassets/SwiftDefaultApps.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lord-Kamina/SwiftDefaultApps/4672a7f028f776697ebd25ac640342f3a497e924/Main Assets.xcassets/SwiftDefaultApps.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /Main Assets.xcassets/SwiftDefaultApps.appiconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lord-Kamina/SwiftDefaultApps/4672a7f028f776697ebd25ac640342f3a497e924/Main Assets.xcassets/SwiftDefaultApps.appiconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "SwiftDefaultApps", 6 | products: [ 7 | .executable(name: "Prefpane", targets: ["SWDA-Prefpane"]), 8 | .executable(name: "CLI", targets: ["SWDA-CLI"]) 9 | ], 10 | dependencies: [ 11 | .package(url: "https://github.com/Lord-Kamina/SwiftCLI", from: Version("2.0.3+swift5")) 12 | ], 13 | targets: [ 14 | .target( 15 | name: "SWDA-Common", 16 | path: "Sources", 17 | sources: ["Common Sources/"] 18 | ), 19 | .target( 20 | name: "DummyApp", 21 | path: "Sources", 22 | sources: ["DummyApp/"] 23 | ), 24 | .target( 25 | name: "SWDA-CLI", 26 | dependencies: ["SwiftCLI", "SWDA-Common", "DummyApp"], 27 | path: "Sources", 28 | sources: ["CLI Components/"] 29 | ), 30 | .target( 31 | name: "SWDA-Prefpane", 32 | dependencies: ["SWDA-Common", "DummyApp"], 33 | path: "Sources", 34 | sources: ["Prefpane Sources/"] 35 | ) 36 | ], 37 | swiftLanguageVersions: [.v4, .v4_2, .v5] 38 | ) 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ANNOUNCEMENT: I have recently noticed Apple has introduced replacements for the deprecated APIs used in this prefpane. As a result, slow as it may be, I plan to resume development. I will be taking PRs as well, if somebody wishes to contribute. 2 | ======== 3 | 4 | SwiftDefaultApps 5 | ======== 6 | 7 | This Preference pane is chiefly intended to be a modern replacement for the amazing RCDefaultApp developed way back when by Carl Lindberg, which stopped working in 10.12 due to deprecation of ObjC Garbage collection. 8 | Additionally, I guess it was a good way to teach myself Swift. 9 | 10 | Feel free to contribute, comment or report issues at https://github.com/Lord-Kamina/SwiftDefaultApps. 11 | 12 | ## Installing & Uninstalling 13 | 14 | * Download latest release from https://github.com/Lord-Kamina/SwiftDefaultApps/releases 15 | * To install, double click on the `.prefpane`, and you will be prompted to install it. 16 | * To uninstall, simply Ctrl+Click on the Prefpane icon and remove it, or move the `.prefpane` file to the Trash. 17 | 18 | ## Installting with brew 19 | 20 | Run: 21 | 22 | ```bash 23 | brew install swiftdefaultappsprefpane 24 | ``` 25 | 26 | then use Spotlight to open the `SwiftDefaultApps.prefpane`. It will open the System Preferences and you find the app on the bottom of the icons. 27 | 28 | ## How to use the "Do Nothing" app 29 | 30 | The **Do Nothing** dummy app needs to be launched before you can use it in the pref pane. For this, open a terminal and run the following commands: 31 | 32 | ```bash 33 | appDir="/Library/PreferencePanes/SwiftDefaultApps.prefPane/Contents/Resources/ThisAppDoesNothing.app" 34 | 35 | if ! [[ -d "$appDir" ]]; then appDir="$HOME/$appDir"; fi 36 | 37 | # Remove quanrantine flag 38 | xattr -d com.apple.quarantine "$appDir" 39 | # Open the app 40 | open "$appDir" 41 | ``` 42 | 43 | After these 2 steps, the **Do Nothing** app should work when you pick it up. 44 | 45 | ## Usage Notes 46 | 47 | This Preference pane will let you view and change default application associations for basically any URI Scheme and/or filetype in macOS. 48 | The user-interface should be pretty self-explanatory; but, there are some things that might require an explanation: 49 | 50 | - Selecting any URI Scheme or File type (always represented by a UTI), will give you a list of all valid applications for each LaunchServices role. This data is generated by LaunchServices itself. 51 | - There's two additional options: 52 | - One of them is **"Do Nothing"**, what this does is register the item to be handled by a dummy application which basically does nothing. Its only function is being able to open any URI Scheme or UTI whatsoever, printing a line to the console (specifying whatever it was that launched it) and immediately quitting. By default this application should be located in the `Resources` folder inside the prefpane bundle; SwiftDefaultApps will, however, also look for it in the directory it is itself located in (for the CLI version) and every `Applications`folder in the computer. 53 | - The second is **"Other..."**, which obviously will allow you to select an application not in the list; with a caveat. In recent versions of macOS, the LaunchServices have become quite a bit smarter under the hood than they used to be. In practice, what this means is that although you can choose *any* application to handle anything, only valid associations will be preserved, as the LaunchServices is permanently looking for invalid or stale associations and removing them. 54 | - One final note: In the URI Schemes tab, you have the option of adding a custom URI Scheme (Removing them on demand is neither possible nor necessary, due to the same thing explained above). This options is provided for completeness' sake; you should virtually never need to use it, since Launch Services should be able to properly detect any valid URL Handlers. As a further cautionary note: If you add a custom URI Scheme when it is not needed, you may not be able to remove it except by uninstalling and reinstalling SwiftDefaultApps. Why? Because new schemes are by default associated to *"Do Nothing", which means Launch Services will always find a valid handler as long as SwiftDefaultApps is installed. 55 | 56 | ### How to Find Out File UTI 57 | 58 | Run in your terminal the following command (replace my_song.mp3 by your file): 59 | 60 | `mdls -name kMDItemContentType -name kMDItemContentTypeTree -name kMDItemKind my_song.mp3` 61 | 62 | ## Acknowledgments & Attributions 63 | 64 | - Using jakeheis' SwiftCLI 2.0 as a base for the CLI version located inside the bundle. (https://github.com/jakeheis/SwiftCLI) 65 | - Using AMTourky's DRYView canned views system (https://github.com/AMTourky/CocoaBindingDryView-ReusableViews/) 66 | - Using ZamzamKit's SynchronizedArray (https://github.com/ZamzamInc/ZamzamKit) 67 | - Icon made using the following resources: 68 | - Brush, Ruler and Pencil designed by Freepik. (http://www.freepik.com/free-vector/background-of-back-to-school_769298.htm) 69 | - Magnifying glass frame designed by Balintseby / Freepik (http://www.freepik.com/free-vector/realistic-magnifying-glass_789215.htm) 70 | - Magnifying glass crystal designed by Freepik (http://www.freepik.com/free-vector/crystal-frames-collection_724172.htm) 71 | - Gears designed by Freepik (http://www.freepik.com/free-vector/gray-background-of-gear_956712.htm) 72 | 73 | ## Current Version 74 | 75 | - Version: 2.0.1 76 | - Date: 2019-07-26 77 | 78 | ## Known Issues 79 | 80 | See https://github.com/Lord-Kamina/SwiftDefaultApps/issues 81 | 82 | ## TO DO 83 | 84 | - Localizations 85 | 86 | # Release Notes 87 | 88 | ## [2.0.1] - 2019-07-26 89 | + ### Fixed 90 | + CLI: Crash when displaying the results for setHandler with shortcuts such as internet, browser, email, etc. 91 | 92 | ## [2.0.0] - 2019-06-12 93 | + ### Added 94 | + Signed prefpane, CLI and Dummy apps. 95 | + Both the prefpane and the CLI version will now automatically try to locate ThisAppDoesNothing.app if it does not appear registered with launch services. 96 | + ### Changed 97 | + SwiftCLI is now built and linked as a static library instead of a framework. 98 | + Updated to Swift 5 99 | + The content array is now populated by overriding `didSelect()` instead of relying on an arbitrary sleep timer. 100 | + Changed the CLI app's name from lsreg to swda. 101 | + Messages in both the CLI and Preference Pane are now a bit more verbose. 102 | + Under the hood, folded most of the app's feedback to the user into a single `displayAlert()` function. 103 | + ### Fixed 104 | + Updated the Swift Package Manager manifest to version 5. 105 | + Updated the SynchronizedArray code and corrected the attribution; Before, I had inadvertently credited an unrelated project with the same name. 106 | + Various small optimizations, fixes and text corrections. 107 | 108 | ## [1.1.3] - 2018-11-15 109 | + ### Fixed 110 | + Bug causing crash on related to force-unwrapping bundleIdentifier. 111 | 112 | ## [1.1.2] - 2018-10-07 113 | + ### Changed 114 | + Migrated to Swift 4.2 115 | + Force Static linking of standard library, which should fix issues running on Mojave. 116 | 117 | ## [1.1.1] - 2018-04-15 118 | + ### Changed 119 | + Small changes for Swift 4.1. 120 | 121 | ## [1.1.0] - 2017-09-27 122 | + ### Changed 123 | + Migrated code to Swift 4. 124 | + ### Fixed 125 | + Fixed an unwrapped Optional when changing associations in the "Applications" tab. 126 | + Fixed CLI tool. 127 | + Some other cleanups. 128 | 129 | ## [1.0.0] - 2017-05-01 130 | + ### Added 131 | + Initial release! 132 | + ### Changed 133 | + ### Fixed 134 | -------------------------------------------------------------------------------- /Resources/DummyApp_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Do Nothing 9 | CFBundleDocumentTypes 10 | 11 | 12 | CFBundleTypeName 13 | All Files 14 | CFBundleTypeRole 15 | Editor 16 | LSItemContentTypes 17 | 18 | public.item 19 | 20 | LSTypeIsPackage 21 | 0 22 | 23 | 24 | CFBundleTypeIconFile 25 | 26 | CFBundleTypeName 27 | All URLS 28 | CFBundleTypeRole 29 | Viewer 30 | LSItemContentTypes 31 | 32 | public.url 33 | 34 | 35 | 36 | CFBundleExecutable 37 | $(EXECUTABLE_NAME) 38 | CFBundleIconFile 39 | Unsupported.icns 40 | CFBundleIdentifier 41 | $(PRODUCT_BUNDLE_IDENTIFIER) 42 | CFBundleInfoDictionaryVersion 43 | 6.0 44 | CFBundleName 45 | $(PRODUCT_NAME) 46 | CFBundlePackageType 47 | APPL 48 | CFBundleShortVersionString 49 | 1.0 50 | CFBundleURLTypes 51 | 52 | 53 | CFBundleTypeRole 54 | Viewer 55 | CFBundleURLName 56 | URL Blanket Scheme 57 | CFBundleURLSchemes 58 | 59 | * 60 | 61 | 62 | 63 | CFBundleVersion 64 | 1 65 | LSBackgroundOnly 66 | 67 | LSMinimumSystemVersion 68 | $(MACOSX_DEPLOYMENT_TARGET) 69 | NSHumanReadableCopyright 70 | Copyright © 2017 Gregorio Litenstein Goldzweig. All rights reserved. 71 | NSMainNibFile 72 | MainMenu 73 | NSPrincipalClass 74 | NSApplication 75 | UTImportedTypeDeclarations 76 | 77 | 78 | UTTypeDescription 79 | Catch all Files 80 | UTTypeIdentifier 81 | public.item 82 | UTTypeTagSpecification 83 | 84 | public.filename-extension 85 | 86 | * 87 | 88 | 89 | 90 | 91 | UTTypeDescription 92 | Catch all URLs 93 | UTTypeIdentifier 94 | public.url 95 | UTTypeTagSpecification 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /Resources/Prefpane_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | $(CURRENT_PROJECT_VERSION) 19 | CFBundleVersion 20 | 334 21 | NSHumanReadableCopyright 22 | Copyright © 2017 Gregorio Litenstein Goldzweig. All rights reserved. 23 | NSMainNibFile 24 | SWDAPrefpaneMain 25 | NSPrefPaneIconFile 26 | 27 | NSPrefPaneIconLabel 28 | SwiftDefaultApps 29 | NSPrincipalClass 30 | SWDAMainPrefPane 31 | 32 | 33 | -------------------------------------------------------------------------------- /Resources/SwiftCLI_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Resources/Unsupported.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lord-Kamina/SwiftDefaultApps/4672a7f028f776697ebd25ac640342f3a497e924/Resources/Unsupported.icns -------------------------------------------------------------------------------- /SWDA Prefpane.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SWDA Prefpane.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SWDA Prefpane.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildSystemType 6 | Latest 7 | 8 | 9 | -------------------------------------------------------------------------------- /SWDA Prefpane.xcodeproj/project.xcworkspace/xcuserdata/Koji.xcuserdatad/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildLocationStyle 6 | UseAppPreferences 7 | BuildSystemType 8 | Latest 9 | CustomBuildLocationType 10 | RelativeToDerivedData 11 | DerivedDataLocationStyle 12 | Default 13 | EnabledFullIndexStoreVisibility 14 | 15 | IssueFilterStyle 16 | ShowActiveSchemeOnly 17 | LiveSourceIssuesEnabled 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /SWDA Prefpane.xcodeproj/xcshareddata/xcschemes/Build Prefpane.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 36 | 37 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 57 | 58 | 59 | 60 | 61 | 62 | 72 | 73 | 75 | 78 | 79 | 85 | 86 | 87 | 88 | 89 | 90 | 94 | 95 | 96 | 102 | 103 | 104 | 105 | 109 | 110 | 111 | 112 | 113 | 114 | 120 | 121 | 123 | 126 | 127 | 133 | 134 | 135 | 136 | 137 | 138 | 142 | 143 | 144 | 150 | 151 | 152 | 153 | 155 | 156 | 159 | 160 | 161 | -------------------------------------------------------------------------------- /SWDA Prefpane.xcodeproj/xcshareddata/xcschemes/Make Release & Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 50 | 51 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 86 | 87 | 89 | 92 | 93 | 99 | 100 | 101 | 102 | 103 | 104 | 108 | 109 | 110 | 116 | 117 | 118 | 119 | 123 | 124 | 125 | 126 | 127 | 128 | 134 | 135 | 137 | 140 | 141 | 147 | 148 | 149 | 150 | 151 | 152 | 156 | 157 | 158 | 164 | 165 | 166 | 167 | 169 | 170 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /Sources/CLI Components/getApps.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return., Gregorio Litenstein. 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 10 | import Foundation 11 | import SwiftCLI 12 | 13 | 14 | class GetApps: Command { 15 | 16 | let name = "getApps" 17 | let signature = "" 18 | let shortDescription = "Returns a list of all registered applications." 19 | 20 | func execute(arguments: CommandArguments) throws { 21 | 22 | if let output = copyStringArrayAsString(LSWrappers.copyAllApps()) { 23 | print(output) 24 | } 25 | else { throw CLIError.error("SwiftDefaultApps ERROR: Couldn't generate the list of installed applications.") } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/CLI Components/getHandler.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return., Gregorio Litenstein. 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | import Foundation 10 | import SwiftCLI 11 | 12 | class ReadCommand: OptionCommand { 13 | 14 | let name = "getHandler" 15 | let signature = "" 16 | let shortDescription = "Returns the default application registered for the URI Scheme or you specify." 17 | 18 | private var kind: String = "" 19 | private var getAll = false; 20 | private var contentType: String? = nil 21 | private var handler: String? = nil 22 | private var roles: Dictionary = ["editor":LSRolesMask.editor,"viewer":LSRolesMask.viewer,"shell":LSRolesMask.shell,"all":LSRolesMask.all] 23 | private var role: LSRolesMask = LSRolesMask.all 24 | 25 | func setupOptions(options: OptionRegistry) { 26 | options.addGroup(name:"type", required:true, conflicting:true) 27 | 28 | options.add(keys: ["--UTI"], usage: "Return the default application for ", valueSignature: "subtype", group:"type") { [unowned self] (value) in 29 | self.contentType = value 30 | self.kind = "UTI" 31 | } 32 | options.add(keys: ["--URL"], usage: "Return the default application for ", valueSignature: "subtype", group:"type") { [unowned self] (value) in 33 | self.contentType = value 34 | self.kind = "URL" 35 | } 36 | options.add(flags: ["--internet", "--browser", "--web"], usage: "Returns the default web browser.", group:"type") { 37 | self.contentType = nil 38 | self.kind = "http" 39 | } 40 | options.add(flags: ["--mail", "--email", "--e-mail"], usage: "Returns the default e-mail client.", group:"type") { 41 | self.contentType = nil 42 | self.kind = "mailto" 43 | } 44 | options.add(flags: ["--ftp"], usage: "Returns the default FTP client.", group:"type") { 45 | self.contentType = nil 46 | self.kind = "ftp" 47 | } 48 | options.add(flags: ["--rss"], usage: "Returns the default RSS client.", group:"type") { 49 | self.contentType = nil 50 | self.kind = "RSS" 51 | } 52 | options.add(flags: ["--news"], usage: "Returns the default news client.", group:"type") { 53 | self.contentType = nil 54 | self.kind = "news" 55 | } 56 | options.add(flags: ["--all"], usage: "When this flag is added, a list of all applications registered for that content will printed.") { 57 | self.getAll = true 58 | } 59 | options.add(keys: ["--role"], usage: "--role , specifies the role with which to register the handler. Default is All.", valueSignature: "role") { [unowned self] (value) in 60 | if let temp = self.roles[value.lowercased()] { 61 | self.role = temp 62 | } 63 | else { self.role = [LSRolesMask.viewer,LSRolesMask.editor] } 64 | } 65 | } 66 | 67 | func execute(arguments: CommandArguments) throws { 68 | 69 | switch(kind,getAll) { 70 | 71 | case ("UTI",true),("URL",true): 72 | 73 | if let contentString = self.contentType { 74 | 75 | handler = copyStringArrayAsString( ((kind == "URL") ? LSWrappers.Schemes.copyAllHandlers(contentString) : LSWrappers.UTType.copyAllHandlers(contentString, inRoles: role)) ) 76 | 77 | } 78 | break 79 | 80 | case ("UTI",false),("URL",false): 81 | 82 | if let contentString = self.contentType { 83 | 84 | handler = ((kind == "URL") ? LSWrappers.Schemes.copyDefaultHandler(contentString) : LSWrappers.UTType.copyDefaultHandler(contentString, inRoles: role)) 85 | } 86 | break 87 | case ("http",Bool()),("mailto",Bool()),("ftp",Bool()),("rss",Bool()),("news",Bool()): 88 | 89 | handler = LSWrappers.Schemes.copyDefaultHandler(kind) 90 | 91 | break 92 | 93 | default: 94 | 95 | handler = nil 96 | 97 | break 98 | } 99 | let arg: String 100 | arg = self.contentType ?? "" 101 | 102 | if (nil != handler) { 103 | print(handler!) 104 | } else { throw CLIError.error(("SwiftDefaultApps ERROR: An incompatible combination was used, or no application is registered to handle \(arg)")) } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/CLI Components/getSchemes.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return., Gregorio Litenstein. 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 10 | import Foundation 11 | import SwiftCLI 12 | 13 | class GetSchemes: Command { 14 | 15 | let name = "getSchemes" 16 | let signature = "" 17 | let shortDescription = "Returns a list of all known URI schemes, accompanied by their default handler." 18 | 19 | func execute(arguments: CommandArguments) throws { 20 | 21 | if let output = copyDictionaryAsString(LSWrappers.Schemes.copySchemesAndHandlers()?.sorted(by: { $0.0 < $1.0 })) { 22 | print(output) 23 | } 24 | else { throw CLIError.error("SwiftDefaultApps ERROR: Couldn't generate list of URI Schemes.") } 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/CLI Components/getUTIs.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return., Gregorio Litenstein. 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 10 | import Foundation 11 | import SwiftCLI 12 | 13 | class GetUTIs: Command { 14 | 15 | let name = "getUTIs" 16 | let signature = "" 17 | let shortDescription = "Returns a list of all known UTIs, and their default handler." 18 | 19 | func execute(arguments: CommandArguments) throws { 20 | 21 | if let output = copyDictionaryAsString(LSWrappers.UTType.copyAllUTIs().sorted(by: { $0.0 < $1.0 })) { 22 | print(output) 23 | } 24 | else { throw CLIError.error("SwiftDefaultApps ERROR: Couldn't generate list of UTIs") } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Sources/CLI Components/main.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return., Gregorio Litenstein. 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 10 | import Foundation 11 | import SwiftCLI 12 | CLI.setup(name:"swda", version:"1.0", description:"Utility to retrieve and manipulate default applications in macOS.") 13 | CLI.register(commands: [ ReadCommand(), GetApps(), GetSchemes(), GetUTIs(), SetCommand() ]) 14 | 15 | exit(CLI.go()) 16 | -------------------------------------------------------------------------------- /Sources/CLI Components/setHandler.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return., Gregorio Litenstein. 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 10 | import Foundation 11 | import SwiftCLI 12 | 13 | class SetCommand: OptionCommand { 14 | var failOnUnrecognizedOptions = true 15 | let name = "setHandler" 16 | let signature = "" 17 | let shortDescription = "Sets as the default handler for a given / combination." 18 | private var kind: String = "" 19 | private var contentType: String? = nil 20 | private var inApplication: String = "None" 21 | private var bundleID: String? = nil 22 | private var statusCode: OSStatus = kLSUnknownErr 23 | private var roles: Dictionary = ["editor":LSRolesMask.editor,"viewer":LSRolesMask.viewer,"shell":LSRolesMask.shell,"all":LSRolesMask.all] 24 | private var role: LSRolesMask = LSRolesMask.all 25 | 26 | func setupOptions(options: OptionRegistry) { 27 | options.addGroup(name:"type", required:true, conflicting:true) 28 | options.addGroup(name:"application", required:true, conflicting:true) 29 | options.add(keys: ["--UTI"], usage: "Change the default application for ", valueSignature: "subtype", group:"type") { [unowned self] (value) in 30 | self.contentType = value 31 | self.kind = "UTI" 32 | } 33 | options.add(keys: ["--URL"], usage: "Change the default application for ", valueSignature: "subtype", group:"type") { [unowned self] (value) in 34 | self.contentType = value 35 | self.kind = "URL" 36 | } 37 | 38 | options.add(flags: ["--internet", "--browser", "--web"], usage: "Changes the default web browser.", group:"type") { 39 | self.contentType = nil 40 | self.kind = "http" 41 | } 42 | options.add(flags: ["--mail", "--email", "--e-mail"], usage: "Changes the default e-mail client.", group:"type") { 43 | self.contentType = nil 44 | self.kind = "mailto" 45 | } 46 | options.add(flags: ["--ftp"], usage: "Changes the default FTP client.", group:"type") { 47 | self.contentType = nil 48 | self.kind = "ftp" 49 | } 50 | options.add(flags: ["--rss"], usage: "Changes the default RSS client.", group:"type") { 51 | self.contentType = nil 52 | self.kind = "RSS" 53 | } 54 | options.add(flags: ["--news"], usage: "Changes the default news client.", group:"type") { 55 | self.contentType = nil 56 | self.kind = "news" 57 | } 58 | options.add(keys: ["--app", "--application"], usage: "The to register as default handler. Specifying \"None\" will remove the currently registered handler.", valueSignature: "application", group:"application") { [unowned self] (value) in 59 | self.inApplication = value 60 | } 61 | options.add(keys: ["--role"], usage: "--role , specifies the role with which to register the handler. Default is All.", valueSignature: "role") { [unowned self] (value) in 62 | if let temp = self.roles[value.lowercased()] { 63 | self.role = temp 64 | } 65 | else { self.role = LSRolesMask.all } 66 | } 67 | } 68 | 69 | func execute(arguments: CommandArguments) throws { 70 | statusCode = LSWrappers.getBundleID(self.inApplication, outBundleID: &bundleID) 71 | guard (statusCode == 0) else { throw CLIError.error(LSWrappers.LSErrors.init(value: statusCode).print(argument: (app: inApplication, content: self.contentType ?? self.kind))) } 72 | switch(kind) { 73 | case "UTI","URL": 74 | if let contentString = self.contentType { 75 | statusCode = ((kind == "URL") ? LSWrappers.Schemes.setDefaultHandler(contentString, bundleID!) : LSWrappers.UTType.setDefaultHandler(contentString, bundleID!, self.role)) 76 | } 77 | break 78 | case "http","mailto","ftp","rss","news": 79 | statusCode = LSWrappers.Schemes.setDefaultHandler(kind, bundleID!) 80 | break 81 | 82 | default: 83 | statusCode = kLSUnknownErr 84 | break 85 | } 86 | do { 87 | try displayAlert(error: statusCode, arg1: (bundleID != nil ? bundleID : inApplication), arg2: self.contentType ?? self.kind) 88 | } catch { print(error) } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/Common Sources/LSWrappers.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return., Gregorio Litenstein. 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 10 | import AppKit 11 | 12 | @_silgen_name("_LSCopySchemesAndHandlerURLs") func LSCopySchemesAndHandlerURLs(_: UnsafeMutablePointer, _: UnsafeMutablePointer) -> OSStatus 13 | @_silgen_name("_LSCopyAllApplicationURLs") func LSCopyAllApplicationURLs(_: UnsafeMutablePointer) -> OSStatus; 14 | @_silgen_name("_UTCopyDeclaredTypeIdentifiers") func UTCopyDeclaredTypeIdentifiers() -> NSArray 15 | 16 | /** 17 | Functions wrapping varied Launch Services tasks to be re-used throughout the application. 18 | */ 19 | class LSWrappers { 20 | 21 | /** 22 | Wrapper for commonly-used errors associated to Launch Services. 23 | - invalidBundle: The specified bundle does not have a valid CFBundlePackageType entry. 24 | - invalidScheme: Supplied URI Scheme is malformed or contains invalid characters. 25 | - incompatibleSys: A valid application bundle was found, but it is not compatible with the current version of macOS. 26 | - serverErr: Can't communicate with the Launch Services server. 27 | - appNotFound: Application not found at given path/URL. 28 | - notAnApp: Found item at given path/URL but it is not an application bundle. 29 | - deletedApp: An application bundle was found, but it is currently in the Trash. 30 | - defaultErr: Unknown error, for cases not covered above. 31 | - invalidFileURL: Trying to locate a file with a scheme different from file:// 32 | - noError: No error occured. 33 | */ 34 | internal enum LSErrors:OSStatus { 35 | case invalidBundle = -67857 36 | case invalidScheme = -30774 37 | case incompatibleSys = -10825 38 | case serverErr = -10822 39 | case appNotFound = -10814 40 | case notAnApp = -10811 41 | case defaultErr = -10810 42 | case deletedApp = -10660 43 | case invalidFileURL = 262 44 | 45 | case noError = 0 46 | 47 | init(value: OSStatus) { 48 | switch value { 49 | case -67857: self = .invalidBundle 50 | case -30774: self = .invalidScheme 51 | case -10825: self = .incompatibleSys 52 | case -10822: self = .serverErr 53 | case -10814: self = .appNotFound 54 | case -10811: self = .notAnApp 55 | case -10660: self = .deletedApp 56 | case -10810: self = .defaultErr 57 | case 262: self = .invalidFileURL 58 | 59 | case 0: self = .noError 60 | default: self = .defaultErr 61 | } 62 | 63 | } 64 | /** 65 | Print a user-readable error message for each error code. 66 | 67 | - Parameter argument: 68 | app: The application specified by the user, which could conceivably be a URL, a file-path, a bundle identifier or even a display name. 69 | content: This is only used in the case the user supplies a malformed URI Scheme. 70 | 71 | - Returns: Human-readable error message specifying the problem, or unknown error if the problem is something not accounted for here. 72 | */ 73 | func print(argument: (app: String?, content: String?) = (String(),String())) -> String { 74 | var retValue = (self == .noError ? "SwiftDefaultApps SUCCESS: " : "SwiftDefaultApps ERROR \(self.rawValue): ") 75 | switch self { 76 | case .notAnApp: retValue += "\(argument.app!) is not a valid application." 77 | case .appNotFound: retValue += "No application found for \(argument.app!)" 78 | case .invalidScheme: retValue += "\(argument.content!) is not a valid URI Scheme." 79 | case .invalidFileURL: retValue += "\(argument.app!) is not a valid filesystem URL." 80 | case .deletedApp: retValue += "\(argument.app!) cannot be accessed because it is in the Trash." 81 | case .serverErr: retValue += "There was an error trying to communicate with the Launch Services Server." 82 | case .incompatibleSys: retValue += "\(argument.app!) is not compatible with the currently installed version of macOS." 83 | case .invalidBundle: retValue += "\(argument.app!) is not a valid Package." 84 | case .noError: retValue += "Default handler has succesfully changed to \(argument.app!)" 85 | case .defaultErr: retValue += "An unknown error has occurred." 86 | } 87 | if ((self == .appNotFound || self == .deletedApp) && (argument.app == "None" || argument.app == "Do Nothing" || argument.app == "cl.fail.lordkamina.ThisAppDoesNothing")) { retValue += " (This error occurs when ThisAppDoesNothing.app is not found.)" } 88 | return retValue 89 | } 90 | } 91 | /** 92 | Groups functions dealing with UTIs. 93 | */ 94 | class UTType { 95 | /** 96 | Copies a list of file-extensions for a given UTI. 97 | - Parameter inUTI: A Uniform Type Identifier. 98 | - Returns: An array of strings corresponding to file-extensions for that UTI, or nil. 99 | */ 100 | static func copyExtensionsFor(_ inUTI: String) -> [String]? { 101 | if let result = (UTTypeCopyAllTagsWithClass(inUTI as CFString, kUTTagClassFilenameExtension)?.takeRetainedValue()) { 102 | if let extensions = result as? [String] { 103 | return extensions 104 | } 105 | else { return nil } 106 | } 107 | else { return nil } 108 | } 109 | /** 110 | Copies the bundle identifier of the application currently registered as the default handler for a given UTI. 111 | - Parameter inUTI: A Uniform Type Identifier. 112 | - Parameter inRoles: The specified Launch Services Role to query. Can correspond to "Editor", "Viewer", "Shell" or "None". By default, we are only concerned with viewers and editors (in that order). 113 | - Returns: The Bundle identifier or POSIX path of an application, or nil if no valid handler was found. 114 | */ 115 | static func copyDefaultHandler (_ inUTI:String, inRoles: LSRolesMask = [LSRolesMask.viewer,LSRolesMask.editor], asPath: Bool = true) -> String? { 116 | if let value = LSCopyDefaultRoleHandlerForContentType(inUTI as CFString, inRoles) { 117 | let handlerID = value.takeRetainedValue() as String 118 | if (asPath == true) { 119 | if let handlerURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: handlerID) { 120 | return handlerURL.path 121 | } 122 | else { return nil } 123 | } 124 | else { return handlerID } 125 | } 126 | else { return nil } 127 | } 128 | /** 129 | Creates a list of all currently registered handlers for a given UTI. 130 | - Parameter inUTI: A Uniform Type Identifier. 131 | - Parameter inRoles: The specified Launch Services Role to query. Can correspond to "Editor", "Viewer", "Shell" or "None". By default, we are only concerned with viewers and editors (in that order). 132 | - Returns: An array of strings corresponding to the Bundle identifiers or POSIX paths of all currently registered handlers, or nil of none were found. 133 | */ 134 | static func copyAllHandlers (_ inUTI:String, inRoles: LSRolesMask = [LSRolesMask.viewer,LSRolesMask.editor], asPath: Bool = true) -> Array? { 135 | var handlers: Array = [] 136 | if let value = LSCopyAllRoleHandlersForContentType(inUTI as CFString, inRoles) { 137 | let handlerIDs = (value.takeRetainedValue() as! Array) 138 | if (asPath == true) { 139 | for handlerID in handlerIDs { 140 | if let handlerURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: handlerID) { 141 | handlers.append(handlerURL.path) 142 | } 143 | } 144 | } 145 | else { return handlerIDs } 146 | } 147 | else { return nil } 148 | return (handlers.isEmpty ? nil : handlers) 149 | } 150 | /** 151 | Creates a keyed dictionary of all currently-registered UTIs and their default handler. Excludes abstract entries like references to physical devices and such things we have no use for. 152 | - Returns: A dictionary with UTIs as keys and bundle identifiers as values. 153 | */ 154 | static func copyAllUTIs () -> [String:String] { 155 | let UTIs = (UTCopyDeclaredTypeIdentifiers() as! Array).filter() { UTTypeConformsTo($0 as CFString,"public.item" as CFString) || UTTypeConformsTo($0 as CFString,"public.content" as CFString)} // Ignore UTIs belonging to devices and such. 156 | var handlers:Array = [] 157 | for UTI in UTIs { 158 | if let handler = UTType.copyDefaultHandler(UTI) { 159 | handlers.append(handler) 160 | } 161 | else { 162 | handlers.append("No application set.") 163 | } 164 | } 165 | 166 | return Dictionary.init (keys: UTIs, values: handlers) 167 | } 168 | /** 169 | Changes the default handler for a given UTI. 170 | - See Also: `enum LSErrors` above. 171 | - Parameters: 172 | - inContent: A Uniform Type Identifier. 173 | - inBundleID: A bundle-identifier referring to a valid application bundle. Specifying "None" will disable the default handler for that UTI. 174 | - inRoles: The specified Launch Services Role to modify. Can correspond to "Editor", "Viewer", "Shell" or "None". 175 | - Returns: A status-code. `0` on success, or a value corresponding to various possible errors. 176 | */ 177 | static func setDefaultHandler (_ inContent: String, _ inBundleID: String, _ inRoles: LSRolesMask = LSRolesMask.all) -> OSStatus { 178 | var retval: OSStatus = 0 179 | if (LSWrappers.isAppInstalled(withBundleID: inBundleID) == true) { 180 | retval = LSSetDefaultRoleHandlerForContentType(inContent as CFString, inRoles, inBundleID as CFString) 181 | } 182 | else { retval = kLSApplicationNotFoundErr } 183 | return retval 184 | } 185 | } 186 | /** 187 | Groups functions dealing with URI Schemes. 188 | */ 189 | class Schemes { 190 | /** 191 | Traverses Info dictionaries of possible handlers for an URI Scheme and gets a display name, if available. 192 | - Parameter inScheme: An URI Scheme. 193 | - Returns: A display name, or `nil` if none was found. 194 | */ 195 | static func getNameForScheme (_ inScheme: String) -> String? { 196 | var schemeName: String? = nil 197 | if let handlers = Schemes.copyAllHandlers(inScheme) { 198 | 199 | for handler in handlers { 200 | 201 | if let schemeDicts = (Bundle(path:handler)?.infoDictionary?["CFBundleURLTypes"] as? [[String:AnyObject]]) { 202 | 203 | for schemeDict in (schemeDicts.filter() { (($0["CFBundleURLSchemes"] as? [String])?.contains() {$0.caseInsensitiveCompare(inScheme) == .orderedSame}) == true } ) { 204 | if let name = (schemeDict["CFBundleURLName"] as? String) { 205 | 206 | schemeName = name 207 | return schemeName 208 | 209 | } 210 | else { schemeName = nil } 211 | 212 | } 213 | } 214 | } 215 | 216 | } 217 | return schemeName 218 | } 219 | /** 220 | Creates a list of all currently registered URI Schemes and their default handler. 221 | - Returns: A dictionary with URI Schemes as keys and bundle identifiers as values. 222 | */ 223 | static func copySchemesAndHandlers() -> [String:String]? { 224 | var schemes_array: NSArray? 225 | var apps_array: NSMutableArray? 226 | if (LSCopySchemesAndHandlerURLs(&schemes_array, &apps_array) == 0) { 227 | if let URLArray = (apps_array! as NSArray) as? [URL] { 228 | if let pathsArray = convertAppURLsToPaths(URLArray) { 229 | 230 | let schemesHandlers = Dictionary.init (keys: schemes_array as! [String], values: pathsArray) 231 | return schemesHandlers 232 | } 233 | else { return nil } 234 | 235 | } 236 | 237 | else { return nil } 238 | } 239 | else { return nil } 240 | } 241 | /** 242 | Copies the bundle identifier of the application currently registered as the default handler for a given URI Scheme. 243 | - Parameter inScheme: A valid URI Scheme. 244 | - Returns: The Bundle identifier or POSIX path of an application, or nil if no valid handler was found. 245 | */ 246 | static func copyDefaultHandler (_ inScheme:String, asPath: Bool = true) -> String? { 247 | 248 | if let value = LSCopyDefaultHandlerForURLScheme(inScheme as CFString) { 249 | let handlerID = value.takeRetainedValue() as String 250 | if (asPath == true) { 251 | if let handlerURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: handlerID) { 252 | return handlerURL.path 253 | } 254 | else { return nil } 255 | } 256 | else { return handlerID } 257 | } 258 | else { return nil } 259 | } 260 | /** 261 | Creates a list of all currently registered handlers for a given URI Scheme. 262 | - Parameter inScheme: A valid URI Scheme. 263 | - Returns: An array of strings corresponding to the Bundle identifiers or POSIX paths of all currently registered handlers, or nil of none were found. 264 | */ 265 | static func copyAllHandlers (_ inScheme:String, asPath: Bool = true) -> Array? { 266 | 267 | var handlers: Array = [] 268 | if let value = LSCopyAllHandlersForURLScheme(inScheme as CFString) { 269 | let handlerIDs = (value.takeRetainedValue() as! Array) 270 | if (asPath == true) { 271 | for handlerID in handlerIDs { 272 | if let handlerURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: handlerID) { 273 | handlers.append(handlerURL.path) 274 | } 275 | } 276 | } 277 | else { return handlerIDs } 278 | } 279 | else { return nil } 280 | return (handlers.isEmpty ? nil : handlers) 281 | } 282 | /** 283 | Changes the default handler for a given URI Scheme. 284 | - See Also: `enum LSErrors` above. 285 | - Parameters: 286 | - inScheme: A valid URI Scheme. 287 | - inBundleID: A bundle-identifier referring to a valid application bundle. Specifying "None" will disable the default handler for that URI Scheme. 288 | - Returns: A status-code. `0` on success, or a value corresponding to various possible errors. 289 | */ 290 | static func setDefaultHandler (_ inScheme: String, _ inBundleID: String) -> OSStatus { 291 | var retval: OSStatus = kLSUnknownErr 292 | if ((inScheme =~ /"\\A[a-zA-Z][a-zA-Z0-9.+-]+$") == true) { 293 | if (LSWrappers.isAppInstalled(withBundleID:inBundleID) == true) { 294 | retval = LSSetDefaultHandlerForURLScheme((inScheme as CFString), (inBundleID as CFString)) 295 | } 296 | else { retval = kLSApplicationNotFoundErr } 297 | } 298 | else { retval = Int32(kURLUnsupportedSchemeError) } 299 | return retval 300 | } 301 | } 302 | 303 | /** 304 | Creates a list of all currently registered applications. 305 | - Returns: An array of strings corresponding to the paths of all applications currently registered with Launch Services. 306 | */ 307 | static func copyAllApps () -> Array? { 308 | var apps: NSMutableArray? 309 | if (LSCopyAllApplicationURLs(&apps) == 0) { 310 | if let appURLs = (apps! as NSArray) as? [URL] { 311 | if let pathsArray = convertAppURLsToPaths(appURLs) { 312 | return pathsArray 313 | } 314 | else { return nil } 315 | } 316 | else { return nil } 317 | } 318 | else { return nil } 319 | } 320 | /** 321 | Checks whether a given application is registered with Launch Services. 322 | - Parameter withBundleID: A bundle identifier. 323 | - Returns: `true` if the bundle identifier is registered with Launch Services as an application, `false` otherwise. 324 | */ 325 | static func isAppInstalled (withBundleID: String) -> Bool { 326 | if (withBundleID == "cl.fail.lordkamina.ThisAppDoesNothing" && NSWorkspace.shared.absolutePathForApplication(withBundleIdentifier: "cl.fail.lordkamina.ThisAppDoesNothing") == nil) { 327 | if (!areWeCLI() && prefPaneLocation() == nil) { return false } 328 | let appSearchPaths: [URL] = [ areWeCLI() ? Bundle.main.bundleURL : Bundle(url: prefPaneLocation()!)!.resourceURL! ] + 329 | FileManager.default.urls(for: FileManager.SearchPathDirectory.applicationDirectory, in: FileManager.SearchPathDomainMask.allDomainsMask) 330 | var appPath: URL? 331 | for path in appSearchPaths { 332 | let appURL = path.appendingPathComponent("ThisAppDoesNothing.app").absoluteURL 333 | if (FileManager.default.isExecutableFile(atPath: appURL.path) == true) { 334 | appPath = appURL 335 | break 336 | } 337 | } 338 | guard appPath != nil else { 339 | return false 340 | } 341 | do { 342 | let ranApp = try NSWorkspace.shared.launchApplication(at: appPath!, options: [NSWorkspace.LaunchOptions.withoutAddingToRecents], configuration: [:]) 343 | if (ranApp.processIdentifier == -1) { 344 | return false 345 | } 346 | else { return true } 347 | } 348 | catch { 349 | return false 350 | } 351 | } 352 | let retval: Bool = ((LSCopyApplicationURLsForBundleIdentifier(withBundleID as CFString,nil)?.takeRetainedValue() as NSArray?) != nil) 353 | return retval 354 | } 355 | /** 356 | Performs a myriad of sanity checks on user input corresponding to a possible application. The main purpose of this function is to make sure we're passing a value as sane as possible to the setHandler functions. 357 | - See Also: `enum LSErrors` above. 358 | - Parameter inParam: The application to locate. It might correspond to a file-system URL, a POSIX path, a display name, a bundle identifier, or "None". 359 | - Parameter outBundleID: This parameter is populated with a bundle identifier if a valid application bundle corresponding to the input parameter was found. 360 | - Returns: A status-code. `0` on success, or a value corresponding to various possible errors. 361 | */ 362 | static func getBundleID (_ inParam: String, outBundleID: inout String?) -> OSStatus { 363 | var errCode = OSStatus() 364 | let filemanager = FileManager.default 365 | if (inParam == "None") { // None is a valid value for our dummy application. 366 | outBundleID = "cl.fail.lordkamina.ThisAppDoesNothing" 367 | return 0 368 | } 369 | if NSWorkspace.shared.absolutePathForApplication(withBundleIdentifier: inParam) != nil { // Check whether we have a valid Bundle ID for an application. 370 | outBundleID = inParam 371 | return 0 372 | } 373 | else if let appPath = NSWorkspace.shared.fullPath(forApplication: inParam) { // Or an application designed by name 374 | if let bundle = Bundle(path:appPath) { 375 | if let type = bundle.getType(outError: &errCode) { 376 | if (type == "APPL" || type == "FNDR") { 377 | if let _ = bundle.bundleIdentifier { 378 | outBundleID = bundle.bundleIdentifier 379 | return 0 380 | } 381 | else { return kLSNotAnApplicationErr } 382 | } 383 | else { return kLSNotAnApplicationErr } 384 | } 385 | else { return errCode } 386 | } 387 | if (filemanager.fileExists(atPath: inParam) == true) { return kLSNotAnApplicationErr } 388 | else { return kLSApplicationNotFoundErr } 389 | } 390 | 391 | else { 392 | if let bundle = Bundle(path: inParam) { // Is it a valid bundle path? 393 | if let type = bundle.getType(outError: &errCode) { 394 | if (type == "APPL") { 395 | if let _ = bundle.bundleIdentifier { 396 | outBundleID = bundle.bundleIdentifier 397 | return 0 398 | } 399 | else { return kLSNotAnApplicationErr } 400 | } 401 | else { return kLSNotAnApplicationErr } 402 | } 403 | else { return errCode } 404 | } 405 | else { 406 | if (filemanager.fileExists(atPath: inParam) == true) { // Maybe it's a valid file path, but not an app bundle? 407 | return kLSNotAnApplicationErr 408 | } 409 | if let url = URL(string: inParam) { // Let's fallback to an URL. 410 | if (url.path != "") { 411 | if (url.isFileURL == true) { 412 | if (filemanager.fileExists(atPath: url.path) == true) { //Is it a valid app URL? 413 | if let bundle = Bundle(url: url) { 414 | if let type = bundle.getType(outError: &errCode) { 415 | if (type == "APPL") { 416 | outBundleID = bundle.bundleIdentifier! 417 | return 0 418 | } 419 | else { return kLSNotAnApplicationErr } 420 | } 421 | else { return errCode } 422 | } 423 | else { return kLSNotAnApplicationErr } // Maybe it's a valid file URL, but not an app bundle? 424 | } 425 | else { 426 | return kLSApplicationNotFoundErr 427 | } // No application found at this location. 428 | } 429 | else { return kLSNotAnApplicationErr } 430 | } 431 | else { 432 | if (url.isFileURL == false) { return Int32(NSFileReadUnsupportedSchemeError) } 433 | } 434 | } 435 | else { 436 | return kLSNotAnApplicationErr 437 | } 438 | } 439 | } 440 | return kLSUnknownErr 441 | } 442 | /** 443 | Creates a list of UTIs and URI Schemes an application claims to be able to handle. 444 | - Note: We perform little if any sanity checks in this function because it is not intended to be exposed to user input. 445 | - Parameter inApp: A POSIX path corresponding to a valid application bundle. 446 | - Returns: A dictionary of sets containing a list of strings corresponding to URI Schemes and Uniform Type Identifiers listed in CFBundleURLTypes and CFBundleDocumentTypes respectively. 447 | */ 448 | static func copySchemesAndUTIsForApp (_ inApp: String) -> [String:[String:Set]]? { 449 | var handledUriSchemes: [String:Set] = ["Viewer":[]] 450 | var handledUTIs: [String:Set] = ["Editor":[],"Viewer":[],"Shell":[]] 451 | var handledTypes: [String:[String:Set]] = [:] 452 | if let infoDict = Bundle(path: inApp)?.infoDictionary { 453 | guard ((infoDict["CFBundlePackageType"] as? String) == "APPL") else { return nil } 454 | if let schemeDicts = (infoDict["CFBundleURLTypes"] as? [[String:AnyObject]]) { 455 | for schemeDict in schemeDicts { 456 | if let schemesArray = (schemeDict["CFBundleURLSchemes"] as? [String]) { 457 | handledUriSchemes["Viewer"]!.formUnion(schemesArray) 458 | } 459 | } 460 | } 461 | if let utiDicts = (infoDict["CFBundleDocumentTypes"] as? [[String:AnyObject]]) { 462 | var utiArray: [String] = [] 463 | for utiDict in utiDicts.filter({ ($0["CFBundleTypeRole"] as? String == "Editor" || $0["CFBundleTypeRole"] as? String == "Viewer" || $0["CFBundleTypeRole"] as? String == "Shell" || $0["CFBundleTypeRole"] == nil) }) { 464 | let typeRole = utiDict["CFBundleTypeRole"] as? String ?? "Viewer" 465 | if let utiArray = (utiDict["LSItemContentTypes"] as? [String]) { 466 | handledUTIs[typeRole]!.formUnion(utiArray) 467 | } 468 | else if let fileExtArray = (utiDict["CFBundleTypeExtensions"] as? [String]) { 469 | for fileExt in fileExtArray { 470 | if let newUTI = (UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, fileExt as CFString, "public.content" as CFString)?.takeRetainedValue() as String?) { 471 | 472 | if ((!UTTypeIsDynamic(newUTI as CFString)) && (handledUTIs[typeRole]!.firstIndex(of:newUTI) == nil)) { 473 | utiArray.append(newUTI) 474 | } 475 | } 476 | } 477 | } 478 | handledUTIs[typeRole]!.formUnion(utiArray) 479 | } 480 | } 481 | } 482 | handledTypes["URIs"] = !handledUriSchemes.isEmpty ? handledUriSchemes : [:] 483 | handledTypes["UTIs"] = !handledUTIs.isEmpty ? handledUTIs : [:] 484 | return handledTypes 485 | } 486 | } 487 | -------------------------------------------------------------------------------- /Sources/Common Sources/commonFuncs.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return., Gregorio Litenstein. 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 10 | import AppKit 11 | 12 | /** 13 | Converts an array of Application URLs to an array of Application Paths. 14 | - Parameter inArray: An array of file-system URLs corresponding to applications. 15 | - Returns: An array of strings corresponding to the POSIX paths of the supplied applications. 16 | */ 17 | func convertAppURLsToPaths (_ inArray:Array?) -> Array? { 18 | if let URLArray = inArray { 19 | var outputArray: Array = [] 20 | for (i, app) in URLArray.enumerated() { 21 | let temp = app.path 22 | outputArray.insert(temp, at:i) 23 | } 24 | return outputArray 25 | } 26 | else { return nil } 27 | } 28 | 29 | /** 30 | Creates a String representation of a Dictionary of Strings. 31 | - Parameter inDict: A dictionary of Strings. 32 | - Returns: A string corresponding to the contents of the supplied dictionary. 33 | */ 34 | func copyDictionaryAsString (_ inDict: [(key:String, value:String)]?) -> String? { 35 | var output = "" 36 | if let temp = inDict { 37 | 38 | for (key, value) in temp { 39 | output+=("\(key)\t\t\t\t\(value)\n") 40 | } 41 | } 42 | else { return nil } 43 | return output 44 | } 45 | 46 | /** 47 | Creates a String representation of an Array of Strings. 48 | - Parameter inArray: An array of strings. 49 | - Parameter separator: The separator to be used. 50 | - Returns: A string corresponding to the contents of the supplied array. 51 | */ 52 | func copyStringArrayAsString (_ inArray: Array?, separator: String = "\n") -> String? { 53 | if let temp = inArray { 54 | return temp.joined(separator:separator) 55 | } 56 | else { return nil } 57 | } 58 | 59 | //// REGEX MATCHING CONVENIENCE OPERATORS. 60 | 61 | infix operator =~ 62 | prefix operator / 63 | 64 | func =~ (string: String, regex: NSRegularExpression?) -> Bool? { 65 | guard let matches = regex?.numberOfMatches(in:string, 66 | options: [], 67 | range: NSMakeRange(0, string.count)) 68 | else { return nil } 69 | return matches > 0 70 | } 71 | 72 | prefix func /(pattern:String) -> NSRegularExpression? { 73 | let options: NSRegularExpression.Options = 74 | NSRegularExpression.Options.dotMatchesLineSeparators 75 | guard let retval = try? NSRegularExpression(pattern:pattern, 76 | options:options) else { return nil } 77 | return retval 78 | } 79 | 80 | 81 | //// EXTENSIONS 82 | 83 | extension String: Error {} 84 | 85 | extension DispatchQueue { 86 | static let labelPrefix = "io.zamzam.ZamzamKit" 87 | static let database = DispatchQueue(label: "\(DispatchQueue.labelPrefix).database", qos: .utility) 88 | } 89 | 90 | public extension Collection { 91 | 92 | /// Element at the given index if it exists. 93 | /// 94 | /// - Parameter index: index of element. 95 | subscript(safe index: Index) -> Element? { 96 | // http://www.vadimbulavin.com/handling-out-of-bounds-exception/ 97 | return indices.contains(index) ? self[index] : nil 98 | } 99 | } 100 | 101 | public extension Collection where Iterator.Element == (String, Any) { 102 | 103 | /// Converts collection of objects to JSON string 104 | var jsonString: String? { 105 | guard JSONSerialization.isValidJSONObject(self), 106 | let stringData = try? JSONSerialization.data(withJSONObject: self, options: []) else { 107 | return nil 108 | } 109 | 110 | return String(data: stringData, encoding: .utf8) 111 | } 112 | } 113 | 114 | public extension Array where Element: Equatable { 115 | 116 | /// Array with all duplicates removed from it. 117 | /// 118 | /// [1, 3, 3, 5, 7, 9].distinct // [1, 3, 5, 7, 9] 119 | var distinct: [Element] { 120 | // https://github.com/SwifterSwift/SwifterSwift 121 | return reduce(into: [Element]()) { 122 | guard !$0.contains($1) else { return } 123 | $0.append($1) 124 | } 125 | } 126 | 127 | /// Remove all duplicates from array. 128 | /// 129 | /// var array = [1, 3, 3, 5, 7, 9] 130 | /// array.removeDuplicates() 131 | /// array // [1, 3, 5, 7, 9] 132 | mutating func removeDuplicates() { 133 | self = distinct 134 | } 135 | 136 | /// Removes the first occurance of the matched element. 137 | /// 138 | /// var array = ["a", "b", "c", "d", "e", "a"] 139 | /// array.remove("a") 140 | /// array // ["b", "c", "d", "e", "a"] 141 | /// 142 | /// - Parameter element: The element to remove from the array. 143 | mutating func remove(_ element: Element) { 144 | guard let index = firstIndex(of: element) else { return } 145 | remove(at: index) 146 | } 147 | } 148 | 149 | extension Bundle { 150 | /** 151 | Copies a bundle's package type, as specified in its Info.plist. 152 | - Parameter outError: Populated with an error-code if the bundle does not correspond to a valid package. 153 | - Returns: The four-letter code specifying the bundle's package type, or `nil` if an error is encountered. 154 | */ 155 | func getType (outError: inout OSStatus) -> String? { 156 | if let info = self.infoDictionary { 157 | if let type = info["CFBundlePackageType"] { 158 | return String(describing: type) 159 | } 160 | else { outError = errSecInvalidBundleInfo; return nil } 161 | } 162 | else { outError = kLSUnknownErr; return nil } 163 | } 164 | } 165 | 166 | extension Dictionary 167 | { 168 | /** 169 | Create a Dictionary by Merging two Arrays. 170 | - Parameter keys: An Array to be used as dictionary keys. 171 | - Parameter values: An Array to be used as values. 172 | */ 173 | public init(keys: [Key], values: [Value]) 174 | { 175 | precondition(keys.count == values.count) 176 | 177 | self.init() 178 | 179 | for (index, key) in keys.enumerated() 180 | { 181 | self[key] = values[index] 182 | } 183 | } 184 | } 185 | 186 | #if Prefpane 187 | extension LSRolesMask { 188 | /** 189 | Bridge our custom type to the actual values used in LaunchServices methods. 190 | */ 191 | init (from value: SourceListRoleTypes) { 192 | switch value { 193 | case .Viewer: self = .viewer 194 | case .Editor: self = .editor 195 | case .Shell: self = .shell 196 | case .All: self = .all 197 | } 198 | } 199 | } 200 | 201 | extension NSControl { 202 | /** Shrinks a text label until it fits in a single line in its container. */ 203 | func fitWidth() { 204 | var absoluteMaxWidth: CGFloat = 0.0 205 | if let tempWidth = (self.superview?.frame.width) { 206 | absoluteMaxWidth = tempWidth 207 | absoluteMaxWidth -= (self.alignmentRectInsets.left + self.alignmentRectInsets.right) 208 | } 209 | 210 | let text = self.stringValue 211 | guard self.font != nil else { return } 212 | var font = self.font! 213 | 214 | if ControllersRef.sharedInstance.originalFonts[self] == nil { 215 | ControllersRef.sharedInstance.originalFonts[self] = font 216 | } 217 | else { font = ControllersRef.sharedInstance.originalFonts[self]! } 218 | 219 | let richText = NSTextFieldCell(textCell:text) 220 | richText.font = font 221 | var neededWidth: CGFloat = richText.cellSize.width 222 | var fontSize = font.pointSize 223 | var newFont = font 224 | guard absoluteMaxWidth > 0.0 else { return } 225 | while (neededWidth >= absoluteMaxWidth) { 226 | guard fontSize > 0.0 else { return } 227 | fontSize -= 0.5 228 | newFont = NSFont(descriptor:font.fontDescriptor, size: fontSize)! 229 | richText.font = newFont 230 | neededWidth = richText.cellSize.width 231 | } 232 | self.setValue(newFont, forKey:"font") 233 | } 234 | } 235 | #endif 236 | 237 | /** Determines whether the app is running in Prefpane or CLI form by checking the Main Bundle Identifier. */ 238 | func areWeCLI() -> Bool { 239 | return (Bundle.main.bundleIdentifier == nil) 240 | } 241 | 242 | /** Determines the URL for the location of the SwiftDefaultApps prefpane, is any. */ 243 | func prefPaneLocation() -> URL? { 244 | if let bundle = Bundle(identifier:"cl.fail.lordkamina.SwiftDefaultApps") { 245 | return bundle.bundleURL 246 | } 247 | let prefpaneSearchPaths: [URL] = FileManager.default.urls(for: FileManager.SearchPathDirectory.preferencePanesDirectory, in: FileManager.SearchPathDomainMask.allDomainsMask) 248 | for path in prefpaneSearchPaths { 249 | if (FileManager.default.isExecutableFile(atPath: path.appendingPathComponent("SwiftDefaultApps.prefpane").absoluteURL.path) == true) { 250 | return path.appendingPathComponent("SwiftDefaultApps.prefpane").absoluteURL 251 | } 252 | } 253 | return nil 254 | } 255 | 256 | /** 257 | Display a modal alert 258 | - Parameter error: A numeric value indicating the error code as per LSWrappers.LSErrors. 259 | - Parameter arg1: String containing info pertaining to the error. 260 | - Parameter arg2: String containing info pertaining to the error. 261 | */ 262 | func displayAlert(error: OSStatus = Int32.min, arg1: String?, arg2: String?) throws { 263 | if (error != 0 && (arg1 == nil || arg2 == nil)) { throw ("Arguments cannot be nil when displaying an error.") } 264 | if (!areWeCLI()) { 265 | let alert = NSAlert() 266 | alert.icon = NSWorkspace.shared.icon(forFile: Bundle(identifier: "cl.fail.lordkamina.SwiftDefaultApps")!.bundlePath) 267 | alert.addButton(withTitle: "OK") 268 | if (error != Int32.min) { 269 | alert.informativeText = LSWrappers.LSErrors(value: error).print(argument: (app: arg1, content: arg2)) 270 | 271 | switch (Int(error)) { 272 | case Int(errSecInvalidBundleInfo)..<0: 273 | alert.messageText = "Error" 274 | alert.alertStyle = .critical 275 | case 0: 276 | alert.messageText = "Success" 277 | alert.alertStyle = .informational 278 | case 1...:alert.messageText = "Success" 279 | alert.alertStyle = .informational 280 | default: 281 | alert.messageText = "Warning" 282 | alert.alertStyle = .warning 283 | } 284 | } 285 | else { 286 | alert.informativeText = "SwiftDefaultApps ERROR: Called displayAlert() with an undefined error code." 287 | } 288 | DispatchQueue.main.async { 289 | alert.runModal() 290 | } 291 | } 292 | else { 293 | if (error < 0) { 294 | throw(LSWrappers.LSErrors.init(value: error).print(argument: (app: arg1, content: arg2))) 295 | } 296 | else if (error == 0) { 297 | print(LSWrappers.LSErrors.init(value: error).print(argument: (app: arg1, content: arg2))) 298 | } 299 | else if (error == Int32.min) { 300 | guard (arg1 != nil) else { throw("SwiftDefaultApps ERROR: Called displayAlert() with an undefined error code and message.") } 301 | print(arg1!) 302 | } 303 | } 304 | } 305 | 306 | /** NSFont extensions to style the different kinds of row in the NSTreeView. */ 307 | extension NSFont { 308 | /** Returns a SmallCaps version of the font it is invoked on. */ 309 | func smallCaps() -> NSFont? { 310 | let settings = [[NSFontDescriptor.FeatureKey.typeIdentifier: kLowerCaseType, NSFontDescriptor.FeatureKey.selectorIdentifier: kLowerCaseSmallCapsSelector]] 311 | let attributes: [NSFontDescriptor.AttributeName: AnyObject] = [NSFontDescriptor.AttributeName.featureSettings: settings as AnyObject, NSFontDescriptor.AttributeName.name: fontName as AnyObject] 312 | return NSFont(descriptor: NSFontDescriptor(fontAttributes: attributes), size: pointSize) 313 | } 314 | /** Returns a **Bold** version of the font it is invoked on. */ 315 | func bold() -> NSFont? { 316 | let fontManager = NSFontManager.shared 317 | return fontManager.convert(self, toHaveTrait: .boldFontMask) 318 | } 319 | /** 320 | Returns an _Italic_ version of the font it is invoked on. 321 | - Note: Not currently used. 322 | */ 323 | func italic() -> NSFont? { 324 | let fontManager = NSFontManager.shared 325 | return fontManager.convert(self, toHaveTrait: .italicFontMask) 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /Sources/DummyApp/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return., Gregorio Litenstein. 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 10 | import Cocoa 11 | 12 | @NSApplicationMain 13 | class AppDelegate: NSObject, NSApplicationDelegate { 14 | 15 | @IBOutlet weak var window: NSWindow! 16 | 17 | @objc func handleGetURLEvent(_ event: NSAppleEventDescriptor!, withReplyEvent: NSAppleEventDescriptor!) { 18 | let urlPassed = event.paramDescriptor(forKeyword: AEKeyword(keyDirectObject))!.stringValue! 19 | NSLog("Dummy application launched in response to URL: \(urlPassed), will now exit.") 20 | } 21 | 22 | @objc func handleOpenEvent(_ event: NSAppleEventDescriptor!, withReplyEvent: NSAppleEventDescriptor!) { 23 | let documentPassed = event.paramDescriptor(forKeyword: AEKeyword(keyDirectObject))!.stringValue! 24 | guard let documentType = try? NSWorkspace.shared.type(ofFile: URL(string:documentPassed)!.path) else { 25 | NSLog("Dummy application launched in response to file with unknown type, will now exit.") 26 | return 27 | } 28 | NSLog("Dummy application launched in response to file of type: \(documentType), will now exit.") 29 | } 30 | 31 | func applicationWillFinishLaunching(_ notification: Notification) { 32 | NSAppleEventManager.shared().setEventHandler(self, andSelector: #selector(AppDelegate.handleGetURLEvent(_:withReplyEvent:)), forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL)) 33 | NSAppleEventManager.shared().setEventHandler(self, andSelector: #selector(AppDelegate.handleOpenEvent(_:withReplyEvent:)), forEventClass: AEEventClass(kCoreEventClass), andEventID: AEEventID(kAEOpenDocuments)) 34 | } 35 | 36 | func applicationDidFinishLaunching(_ aNotification: Notification) { 37 | NSApplication.shared.terminate(self) 38 | } 39 | 40 | func applicationWillTerminate(_ aNotification: Notification) { 41 | // Insert code here to tear down your application 42 | } 43 | 44 | } 45 | 46 | -------------------------------------------------------------------------------- /Sources/Prefpane Sources/Model/SWDAApplicationInfo.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return., Gregorio Litenstein. 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 10 | import AppKit 11 | 12 | 13 | /** Represents information about an application and its associated URI Schemes and UTIs as an object. */ 14 | class SWDAApplicationInfo: NSObject { 15 | var displayName: String? 16 | var appPath: String? 17 | var appURL: String? 18 | var appDescription: String? 19 | var appVersion: String? 20 | var appBundleID: String? 21 | var appIcon: NSImage? 22 | lazy var handlers: [SWDAContentHandler] = { return [] }() 23 | 24 | lazy var handledContent: [String: [String:[SWDAContentHandler]?]] = { 25 | var wrappedHandlers: [String: [String:[SWDAContentHandler]?]] = [:] 26 | if let handlers = LSWrappers.copySchemesAndUTIsForApp(self.appPath!) { 27 | 28 | var wrappedURIHandlers: [String:[SWDAContentHandler]] = ["Viewer":[]] 29 | var wrappedUTIHandlers: [String:[SWDAContentHandler]] = ["Viewer":[],"Editor":[],"Shell":[]] 30 | 31 | for handlerRole in handlers["UTIs"]! { 32 | let role = String(describing:handlerRole.key) 33 | var handler: SWDAContentHandler? 34 | 35 | for utiHandler in handlerRole.value { 36 | 37 | handler = SWDAContentHandler(SWDAContentItem(type:SWDAContentType(rawValue:"UTI")!, utiHandler), appName: self.appBundleID!, role: SourceListRoleTypes(rawValue:role)!) 38 | if let handler = handler { 39 | wrappedUTIHandlers[role]!.append(handler) 40 | } 41 | } 42 | } 43 | 44 | for handlerRole in handlers["URIs"]! { 45 | let role = String(describing:handlerRole.key) 46 | var handler: SWDAContentHandler? 47 | 48 | for urlHandler in handlerRole.value { 49 | 50 | handler = SWDAContentHandler(SWDAContentItem(type:SWDAContentType(rawValue:"URI")!, urlHandler), appName: self.appBundleID!, role: SourceListRoleTypes(rawValue:role)!) 51 | if let handler = handler { 52 | wrappedURIHandlers[role]!.append(handler) 53 | } 54 | } 55 | } 56 | wrappedHandlers["UTIs"] = wrappedUTIHandlers 57 | wrappedHandlers["URIs"] = wrappedURIHandlers 58 | return wrappedHandlers 59 | } 60 | else { return ["URIs":["Viewer":[]],"UTIs":["Editor":[], "Viewer":[], "Shell":[]]] } 61 | }() 62 | 63 | /** Only applications that handle at least one URI Scheme or UTI will show up in the "Applications" tab. */ 64 | lazy var handlesURIs: Bool = { 65 | for role in (self.handledContent["URIs"]!).values { 66 | if !(role!.isEmpty) { return true } 67 | } 68 | return false 69 | }() 70 | lazy var handlesUTIs: Bool = { 71 | for role in (self.handledContent["UTIs"]!).values { 72 | if !(role!.isEmpty) { return true } 73 | } 74 | return false 75 | }() 76 | 77 | init?(_ inParam: String) { 78 | var bundleID: String? 79 | switch inParam { 80 | case "Other...": 81 | self.displayName = "Other..." 82 | self.appVersion = "" 83 | self.appPath = "" 84 | self.appIcon = NSImage(byReferencingFile: "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericApplicationIcon.icns") 85 | default: 86 | let result = LSWrappers.getBundleID(inParam, outBundleID: &bundleID) 87 | if (result == 0) { 88 | let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID!) 89 | guard appURL != nil else { return nil } 90 | if let bundle = Bundle(url: appURL!) { 91 | guard bundle.bundleIdentifier != nil else { 92 | NSLog(LSWrappers.LSErrors.invalidBundle.print(argument: (app: inParam, content:""))) 93 | return nil; 94 | } 95 | self.appBundleID = bundle.bundleIdentifier! 96 | let name = (bundle.object(forInfoDictionaryKey: "CFBundleName") as? String) 97 | let displayName = (bundle.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String) 98 | var tempName: String? 99 | if let displayName = displayName { 100 | tempName = displayName 101 | } 102 | sanity: if let name = bundle.object(forInfoDictionaryKey: "CFBundleName") as? String { // Theoretically, apps should always have a CFBundleName and most of the time, also a CFBundleDisplayName. In practice? Not so much. We should be prepared to handle broken apps. 103 | guard (tempName == nil || tempName == "") else { break sanity } 104 | tempName = name 105 | } 106 | else { 107 | tempName = FileManager.default.displayName(atPath: bundle.bundlePath) 108 | } 109 | self.displayName = tempName 110 | self.appURL = String(describing:bundle.bundleURL) 111 | if let version = bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String { 112 | self.appVersion = version 113 | } 114 | else if let version = bundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String { 115 | self.appVersion = version 116 | } 117 | else { self.appVersion = nil } 118 | self.appPath = bundle.bundlePath 119 | self.appIcon = NSWorkspace.shared.icon(forFile: bundle.bundlePath) 120 | } 121 | else { 122 | NSLog(LSWrappers.LSErrors.init(value:result).print(argument: (app: inParam, content:""))) 123 | return nil 124 | } 125 | } 126 | else { 127 | NSLog(LSWrappers.LSErrors.init(value:result).print(argument: (app: inParam, content:""))) 128 | return nil 129 | } 130 | } 131 | super.init() 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Sources/Prefpane Sources/Model/SWDAContentProtocol.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return., Gregorio Litenstein. 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 10 | import AppKit 11 | 12 | /** Abstract protocol adopted by SWDAContentItem and SWDAApplicationItem, which define the appropriate classes to populate the NSTableView with. */ 13 | internal protocol SWDAContentProtocol: NSObject { 14 | var contentType: SWDAContentType { get set } 15 | var contentName: String { get set } 16 | var contentDescription: String { get set } 17 | var displayName: String { get set } 18 | var contentHandlers: [SWDATreeRow] { get set } 19 | func getDescription() -> String 20 | var appIcon: NSImage? { get } 21 | var getExtensions: String { get } 22 | } 23 | 24 | /** The main class adopting SWDAContentProtocol, represents either a UTI or an URL; it's name, associated handlers, description in the case of URI Schemes and associated file-extensions in the case of UTIs.*/ 25 | class SWDAContentItem: NSObject, SWDAContentProtocol { 26 | /** Generates the NSTreeView displaying all the handlers associated to this content type. Results are sorted alphabetically, with the exception of the special "Other..." and "Do Nothing" entries. */ 27 | 28 | @objc lazy var contentHandlers: [SWDATreeRow] = { 29 | var roles: [SWDATreeRow] = [] 30 | var rolesArray: [SourceListRoleTypes] = [] 31 | switch self.contentType { 32 | case .UTI: 33 | rolesArray = [.Viewer, .Editor, .Shell] 34 | case .URI: 35 | rolesArray = [.Viewer] 36 | case .Application: return [] 37 | } 38 | for role in rolesArray { 39 | var tempChildren: [SWDATreeRow] = [] 40 | let category = SWDATreeRow(role.rawValue) 41 | if let handlerAppNames = (self.contentType == .URI) ? LSWrappers.Schemes.copyAllHandlers(self.contentName, asPath:true) : LSWrappers.UTType.copyAllHandlers(self.contentName, inRoles: LSRolesMask(from: role), asPath:true) { 42 | for app in handlerAppNames { 43 | let handler = SWDAContentHandler(self, appName:app, role:role) 44 | if let tempName = handler.application?.displayName { 45 | let displayName = (tempName.lowercased().range(of:".app") != nil) ? tempName : "\(tempName).app" 46 | if (displayName != "Do Nothing.app") { 47 | let row = SWDATreeRow(displayName, content: handler) 48 | category.addChild(row) 49 | } 50 | } 51 | else { 52 | let displayName = FileManager.default.displayName(atPath: app) 53 | if (displayName != "Do Nothing.app") { 54 | let row = SWDATreeRow(displayName, content: handler) 55 | category.addChild(row) 56 | } 57 | } 58 | } 59 | } 60 | let other = SWDATreeRow("Other...",content: SWDAContentHandler(self, appName: "Other...", role:role)) 61 | category.addChild(other) 62 | let none = SWDATreeRow("Do Nothing",content: SWDAContentHandler(self, appName: "cl.fail.lordkamina.ThisAppDoesNothing", role:role)) 63 | 64 | category.addChild(none) 65 | category.children.sort(){($0.rowTitle < $1.rowTitle) && ($0.rowTitle != "Other..." && $0.rowTitle != "Do Nothing")} 66 | roles.append(category) 67 | } 68 | return roles 69 | }() 70 | @objc var contentDescription: String = "" 71 | @objc var contentName: String = "" 72 | var contentType: SWDAContentType 73 | 74 | /** Initializer. contentDescription is only really used for the "Internet" Tab. */ 75 | init (type: SWDAContentType, _ contentName: String, _ contentDescription: String? = nil) { 76 | self.contentType = type 77 | super.init() 78 | self.setValue(contentName, forKey: "contentName") 79 | if let description = contentDescription { 80 | self.setValue(description, forKey: "contentDescription") 81 | } 82 | else { 83 | self.setValue(self.getDescription(), forKey: "contentDescription") 84 | } 85 | } 86 | 87 | @objc lazy var displayName: String = { 88 | return self.value(forKey:ControllersRef.TabData.displayNameKeyPath) as! String 89 | }() 90 | @objc lazy var getExtensions: String = { 91 | guard self.contentType == .UTI else { return "" } 92 | if let extensions = copyStringArrayAsString(LSWrappers.UTType.copyExtensionsFor(self.contentName), separator:", ") { 93 | return "Extensions: \(extensions)" 94 | } 95 | else { return "Extensions: " } 96 | }() 97 | 98 | func getDescription () -> String { 99 | switch self.contentType { 100 | case .URI: 101 | if let contentDescription = LSWrappers.Schemes.getNameForScheme(self.contentName as String) { 102 | self.setValue(contentDescription, forKey: "contentDescription") 103 | return self.contentDescription 104 | } 105 | else { return "" } 106 | case .UTI: if let desc = (UTTypeCopyDescription(self.contentName as CFString)?.takeRetainedValue() as String?) { 107 | return desc 108 | } 109 | else { return "" } 110 | case .Application: return "" 111 | } 112 | } 113 | @objc lazy var appPath: String = { return "" }() 114 | @objc var appIcon: NSImage? = nil 115 | } 116 | 117 | /** Our other NSObject subclass adopting the SWDAContentProtocol, represents an Application and all of its associated URI Schemes and UTIs. If an application declares no UTIs, it looks for File Extensions and displays the UTI preferred to represent that extension. */ 118 | class SWDAApplicationItem: NSObject, SWDAContentProtocol { 119 | @objc lazy var contentHandlers: [SWDATreeRow] = { 120 | var allHandlers: [SWDATreeRow] = [] 121 | if let bundle = self.appBundleInfo { 122 | let handledContent = bundle.handledContent 123 | if (bundle.handlesURIs == true) { 124 | let URIs = SWDATreeRow("URI Schemes") 125 | let roles = ["Viewer"] 126 | for role in roles { 127 | let row = SWDATreeRow(role) 128 | for handler in ((handledContent["URIs"]![role]!)!) { 129 | let content = handler.content as! SWDAContentItem 130 | let rowName = content.contentName 131 | let child = SWDATreeRow(rowName, content: handler) 132 | row.addChild(child) 133 | } 134 | if (row.children.count > 0) { URIs.addChildren(of: row) } 135 | } 136 | allHandlers.append(URIs) 137 | } 138 | if (bundle.handlesUTIs == true) { 139 | let UTIs = SWDATreeRow("Uniform Type Identifiers") 140 | let roles = ["Viewer", "Editor", "Shell"] 141 | for role in roles { 142 | let row = SWDATreeRow(role) 143 | for handler in ((handledContent["UTIs"]![role]!)!) { 144 | let content = handler.content as! SWDAContentItem 145 | let rowName = content.contentName 146 | let child = SWDATreeRow(rowName, content: handler) 147 | row.addChild(child) 148 | } 149 | if (row.children.count > 0) { UTIs.addChild(row) } 150 | } 151 | allHandlers.append(UTIs) 152 | } 153 | } 154 | return allHandlers 155 | }() 156 | @objc var contentDescription: String = "" 157 | @objc var contentName: String = "" 158 | var contentType: SWDAContentType 159 | @objc var appBundleInfo: SWDAApplicationInfo? 160 | 161 | /** Determine a hashValue from the Bundle ID to prevent duplicate Applications, since in practice Launch Services will not allow us to choose a specific version of an Application but rather choose the best according to its own set of criteria outlined in the Launch Services Programming Guide. */ 162 | override var hash: Int { return (self.appBundleInfo?.appBundleID)?.hash ?? -1 } 163 | 164 | init? (_ app: String) { 165 | self.contentType = .Application 166 | if let appInfo = SWDAApplicationInfo(app) { 167 | self.appBundleInfo = appInfo 168 | guard (appInfo.handlesURIs == true || appInfo.handlesUTIs == true) else { return nil } 169 | } 170 | else { return nil } 171 | super.init() 172 | self.setValue(self.appBundleInfo?.displayName, forKey: "contentName") 173 | self.setValue(self.getDescription(), forKey: "contentDescription") 174 | } 175 | @objc lazy var displayName: String = { 176 | return self.value(forKey:"contentName") as! String 177 | }() 178 | 179 | @objc lazy var appPath: String = { 180 | if let path = self.appBundleInfo?.appPath { 181 | return path 182 | } 183 | else { return "" } 184 | }() 185 | 186 | @objc lazy var appIcon: NSImage? = { 187 | if let icon = self.appBundleInfo?.appIcon { 188 | return icon 189 | } 190 | else { return nil } 191 | }() 192 | @objc var getExtensions: String = "" 193 | func getDescription () -> String { 194 | if let version = self.appBundleInfo?.appVersion { 195 | return "Version: \(version)" 196 | } 197 | else { return "" } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /Sources/Prefpane Sources/Model/SWDAHandlersModel.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return., Gregorio Litenstein. 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 10 | import Foundation 11 | 12 | /** Represent LSRolesMask in a more convenient format for us. */ 13 | enum SourceListRoleTypes:String { 14 | case Editor, Viewer, Shell, All 15 | } 16 | 17 | /** Represent possible kinds of NSObject conforming to SWDAContentProtocol */ 18 | internal enum SWDAContentType: String { 19 | case UTI, URI, Application 20 | } 21 | 22 | 23 | /** Our main model class, tasked with getting and populating the content arrays associated with each tab. */ 24 | class SWDAHandlersModel: NSObject { 25 | @objc static var allUTIs: [SWDAContentItem]? 26 | @objc static var allSchemes: [SWDAContentItem]? 27 | @objc static var internetSchemes: [SWDAContentItem]? 28 | @objc static var allApps: [SWDAApplicationItem]? 29 | 30 | /** 31 | This function is indirectly called by the contentArray variable in each SWDATabTemplate instance, it's responsible for asynchronously populating each model array with the appropriate content and sending messages for the ProgressAlert to update itself. 32 | - Parameter view: The SWDATabView instance that requested the content. We pass this so we can then modify its backing-store in a KVO-compliant manner via setValue(_:forKey:) 33 | - Parameter type: String representation of what that SWDATabTemplate is asking for. 34 | - Parameter competionHandler: Code block to execute upon completion. In practice, what this does is invoke setValue(_:forKey:) with the resulting array as the value. 35 | */ 36 | static internal func populateContentArray(in view: SWDATabTemplate, with type: String, completionHandler: @escaping () -> Void) { 37 | let progressAlert = ProgressAlert() 38 | 39 | guard (ControllersRef.sharedInstance.thePrefPane?.mainCustomView.window != nil) else { return } 40 | let window = (ControllersRef.sharedInstance.thePrefPane?.mainCustomView.window)! 41 | 42 | progressAlert.beginSheetModal(for: window, completionHandler: nil) 43 | 44 | var codeBlock: (_ : Int) -> Void = { index in } 45 | var sourceItems: [String] = [] 46 | var sourceDescriptions: [String:String] = [:] 47 | // By using the SynchronizedArray class, we can concurrently c breyt6 te the larger Content Arrays across multiple threads, in a safe manner. 48 | var outputItems: SynchronizedArray 49 | if (type == "Applications") { 50 | outputItems = SynchronizedArray([SWDAApplicationItem]()) 51 | } else { 52 | outputItems = SynchronizedArray([SWDAContentItem]()) 53 | } 54 | switch type { 55 | case "Internet": 56 | sourceItems = ["http","mailto","news","rss","ftp","im"] 57 | sourceDescriptions = ["http":"Web Browser","mailto":"E-Mail","news":"News","rss":"RSS","ftp":"FTP","im":"Instant Messaging"] // In practice, these are just shortcuts for the most commonly-used URI Schemes. 58 | codeBlock = { 59 | index in 60 | let i = Int(index) 61 | outputItems.append(SWDAContentItem(type:SWDAContentType(rawValue: "URI")!, sourceItems[i], sourceDescriptions[sourceItems[i]])) 62 | DispatchQueue.main.async { [weak progressAlert] in 63 | progressAlert?.increment(by: 1) 64 | } 65 | } 66 | case "URIs": 67 | if let schemesHandlers = LSWrappers.Schemes.copySchemesAndHandlers() { 68 | sourceItems = Array(schemesHandlers.keys) 69 | } 70 | else { return } 71 | codeBlock = { 72 | index in 73 | let i = Int(index) 74 | if sourceItems[i] != "*" { 75 | outputItems.append(SWDAContentItem(type:SWDAContentType(rawValue: "URI")!, sourceItems[i])) 76 | } 77 | DispatchQueue.main.async { [weak progressAlert] in 78 | progressAlert?.increment(by: 1) 79 | } 80 | } 81 | case "UTIs": 82 | sourceItems = Array(LSWrappers.UTType.copyAllUTIs().keys) 83 | codeBlock = { 84 | index in 85 | let i = Int(index) 86 | outputItems.append(SWDAContentItem(type:SWDAContentType(rawValue: "UTI")!, sourceItems[i])) 87 | DispatchQueue.main.async { [weak progressAlert] in 88 | progressAlert?.increment(by: 1) 89 | } 90 | } 91 | case "Applications": 92 | if let apps = LSWrappers.copyAllApps() { 93 | sourceItems = apps 94 | } 95 | else { return } 96 | codeBlock = { 97 | index in 98 | let i = Int(index) 99 | let app = sourceItems[i] 100 | if let wrappedApp = SWDAApplicationItem(app) { 101 | if (outputItems .first() { ($0 as! SWDAApplicationItem).appBundleInfo?.appBundleID == wrappedApp.appBundleInfo?.appBundleID } == nil) { outputItems.append(wrappedApp) } 102 | } 103 | DispatchQueue.main.async { [weak progressAlert] in 104 | progressAlert?.increment(by: 1) 105 | } 106 | } 107 | default: return 108 | } 109 | progressAlert.maxValue = Double(sourceItems.count) 110 | 111 | DispatchQueue.global(qos: .userInteractive).async { 112 | DispatchQueue.concurrentPerform(iterations: sourceItems.count, execute: codeBlock) 113 | 114 | DispatchQueue.main.sync { [weak window] in // Using sync for the finalization of the array ensures all the data is in place before reporting completion to the View Controller. 115 | let finalArray = outputItems.innerArray 116 | setValue(finalArray, forKey: ControllersRef.TabData.modelArrayKeyPath) 117 | window?.endSheet(progressAlert.window) 118 | completionHandler() 119 | } 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Sources/Prefpane Sources/Model/SynchronizedArray.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SynchronizedArray.swift 3 | // ZamzamKit 4 | // http://basememara.com/creating-thread-safe-arrays-in-swift/ 5 | // 6 | // Created by Basem Emara on 2/27/17. 7 | // Copyright © 2017 Zamzam Inc. All rights reserved. 8 | // 9 | // 10 | 11 | import Foundation 12 | 13 | /// A thread-safe array. 14 | public class SynchronizedArray { 15 | private let queue = DispatchQueue(label: "\(DispatchQueue.labelPrefix).SynchronizedArray", qos: .utility, attributes: .concurrent) 16 | private var array = [Element]() 17 | 18 | public init() { } 19 | 20 | public convenience init(_ array: [Element]) { 21 | self.init() 22 | self.array = array 23 | } 24 | } 25 | 26 | // MARK: - Properties 27 | 28 | public extension SynchronizedArray { 29 | /// Extract the inner array. 30 | var innerArray: [Element] { return self.array } 31 | 32 | /// The first element of the collection. 33 | var first: Element? { 34 | var result: Element? 35 | queue.sync { result = self.array.first } 36 | return result 37 | } 38 | 39 | /// The last element of the collection. 40 | var last: Element? { 41 | var result: Element? 42 | queue.sync { result = self.array.last } 43 | return result 44 | } 45 | 46 | /// The number of elements in the array. 47 | var count: Int { 48 | var result = 0 49 | queue.sync { result = self.array.count } 50 | return result 51 | } 52 | 53 | /// A Boolean value indicating whether the collection is empty. 54 | var isEmpty: Bool { 55 | var result = false 56 | queue.sync { result = self.array.isEmpty } 57 | return result 58 | } 59 | 60 | /// A textual representation of the array and its elements. 61 | var description: String { 62 | var result = "" 63 | queue.sync { result = self.array.description } 64 | return result 65 | } 66 | } 67 | 68 | // MARK: - Immutable 69 | 70 | public extension SynchronizedArray { 71 | 72 | /// Returns the first element of the sequence that satisfies the given predicate. 73 | /// 74 | /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match. 75 | /// - Returns: The first element of the sequence that satisfies predicate, or nil if there is no element that satisfies predicate. 76 | func first(where predicate: (Element) -> Bool) -> Element? { 77 | var result: Element? 78 | queue.sync { result = self.array.first(where: predicate) } 79 | return result 80 | } 81 | 82 | /// Returns the last element of the sequence that satisfies the given predicate. 83 | /// 84 | /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match. 85 | /// - Returns: The last element of the sequence that satisfies predicate, or nil if there is no element that satisfies predicate. 86 | func last(where predicate: (Element) -> Bool) -> Element? { 87 | var result: Element? 88 | queue.sync { result = self.array.last(where: predicate) } 89 | return result 90 | } 91 | 92 | /// Returns an array containing, in order, the elements of the sequence that satisfy the given predicate. 93 | /// 94 | /// - Parameter isIncluded: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element should be included in the returned array. 95 | /// - Returns: An array of the elements that includeElement allowed. 96 | func filter(_ isIncluded: @escaping (Element) -> Bool) -> SynchronizedArray { 97 | var result: SynchronizedArray? 98 | queue.sync { result = SynchronizedArray(self.array.filter(isIncluded)) } 99 | return result ?? self 100 | } 101 | 102 | /// Returns the first index in which an element of the collection satisfies the given predicate. 103 | /// 104 | /// - Parameter predicate: A closure that takes an element as its argument and returns a Boolean value that indicates whether the passed element represents a match. 105 | /// - Returns: The index of the first element for which predicate returns true. If no elements in the collection satisfy the given predicate, returns nil. 106 | func firstIndex(where predicate: (Element) -> Bool) -> Int? { 107 | var result: Int? 108 | queue.sync { result = self.array.firstIndex(where: predicate) } 109 | return result 110 | } 111 | 112 | /// Returns the elements of the collection, sorted using the given predicate as the comparison between elements. 113 | /// 114 | /// - Parameter areInIncreasingOrder: A predicate that returns true if its first argument should be ordered before its second argument; otherwise, false. 115 | /// - Returns: A sorted array of the collection’s elements. 116 | func sorted(by areInIncreasingOrder: (Element, Element) -> Bool) -> SynchronizedArray { 117 | var result: SynchronizedArray? 118 | queue.sync { result = SynchronizedArray(self.array.sorted(by: areInIncreasingOrder)) } 119 | return result ?? self 120 | } 121 | 122 | /// Returns an array containing the results of mapping the given closure over the sequence’s elements. 123 | /// 124 | /// - Parameter transform: A closure that accepts an element of this sequence as its argument and returns an optional value. 125 | /// - Returns: An array of the non-nil results of calling transform with each element of the sequence. 126 | func map(_ transform: @escaping (Element) -> ElementOfResult) -> [ElementOfResult] { 127 | var result = [ElementOfResult]() 128 | queue.sync { result = self.array.map(transform) } 129 | return result 130 | } 131 | 132 | /// Returns an array containing the non-nil results of calling the given transformation with each element of this sequence. 133 | /// 134 | /// - Parameter transform: A closure that accepts an element of this sequence as its argument and returns an optional value. 135 | /// - Returns: An array of the non-nil results of calling transform with each element of the sequence. 136 | func compactMap(_ transform: (Element) -> ElementOfResult?) -> [ElementOfResult] { 137 | var result = [ElementOfResult]() 138 | queue.sync { result = self.array.compactMap(transform) } 139 | return result 140 | } 141 | 142 | /// Returns the result of combining the elements of the sequence using the given closure. 143 | /// 144 | /// - Parameters: 145 | /// - initialResult: The value to use as the initial accumulating value. initialResult is passed to nextPartialResult the first time the closure is executed. 146 | /// - nextPartialResult: A closure that combines an accumulating value and an element of the sequence into a new accumulating value, to be used in the next call of the nextPartialResult closure or returned to the caller. 147 | /// - Returns: The final accumulated value. If the sequence has no elements, the result is initialResult. 148 | func reduce(_ initialResult: ElementOfResult, _ nextPartialResult: @escaping (ElementOfResult, Element) -> ElementOfResult) -> ElementOfResult { 149 | var result: ElementOfResult? 150 | queue.sync { result = self.array.reduce(initialResult, nextPartialResult) } 151 | return result ?? initialResult 152 | } 153 | 154 | /// Returns the result of combining the elements of the sequence using the given closure. 155 | /// 156 | /// - Parameters: 157 | /// - initialResult: The value to use as the initial accumulating value. 158 | /// - updateAccumulatingResult: A closure that updates the accumulating value with an element of the sequence. 159 | /// - Returns: The final accumulated value. If the sequence has no elements, the result is initialResult. 160 | func reduce(into initialResult: ElementOfResult, _ updateAccumulatingResult: @escaping (inout ElementOfResult, Element) -> Void) -> ElementOfResult { 161 | var result: ElementOfResult? 162 | queue.sync { result = self.array.reduce(into: initialResult, updateAccumulatingResult) } 163 | return result ?? initialResult 164 | } 165 | 166 | /// Calls the given closure on each element in the sequence in the same order as a for-in loop. 167 | /// 168 | /// - Parameter body: A closure that takes an element of the sequence as a parameter. 169 | func forEach(_ body: (Element) -> Void) { 170 | queue.sync { self.array.forEach(body) } 171 | } 172 | 173 | /// Returns a Boolean value indicating whether the sequence contains an element that satisfies the given predicate. 174 | /// 175 | /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value that indicates whether the passed element represents a match. 176 | /// - Returns: true if the sequence contains an element that satisfies predicate; otherwise, false. 177 | func contains(where predicate: (Element) -> Bool) -> Bool { 178 | var result = false 179 | queue.sync { result = self.array.contains(where: predicate) } 180 | return result 181 | } 182 | 183 | /// Returns a Boolean value indicating whether every element of a sequence satisfies a given predicate. 184 | /// 185 | /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value that indicates whether the passed element satisfies a condition. 186 | /// - Returns: true if the sequence contains only elements that satisfy predicate; otherwise, false. 187 | func allSatisfy(_ predicate: (Element) -> Bool) -> Bool { 188 | var result = false 189 | queue.sync { result = self.array.allSatisfy(predicate) } 190 | return result 191 | } 192 | } 193 | 194 | // MARK: - Mutable 195 | 196 | public extension SynchronizedArray { 197 | 198 | /// Adds a new element at the end of the array. 199 | /// 200 | /// The task is performed asynchronously due to thread-locking management. 201 | /// 202 | /// - Parameters: 203 | /// - element: The element to append to the array. 204 | /// - completion: The block to execute when completed. 205 | func append(_ element: Element, completion: (() -> Void)? = nil) { 206 | queue.async(flags: .barrier) { 207 | self.array.append(element) 208 | DispatchQueue.main.async { completion?() } 209 | } 210 | } 211 | 212 | /// Adds new elements at the end of the array. 213 | /// 214 | /// The task is performed asynchronously due to thread-locking management. 215 | /// 216 | /// - Parameters: 217 | /// - element: The elements to append to the array. 218 | /// - completion: The block to execute when completed. 219 | func append(_ elements: [Element], completion: (() -> Void)? = nil) { 220 | queue.async(flags: .barrier) { 221 | self.array += elements 222 | DispatchQueue.main.async { completion?() } 223 | } 224 | } 225 | 226 | /// Inserts a new element at the specified position. 227 | /// 228 | /// The task is performed asynchronously due to thread-locking management. 229 | /// 230 | /// - Parameters: 231 | /// - element: The new element to insert into the array. 232 | /// - index: The position at which to insert the new element. 233 | /// - completion: The block to execute when completed. 234 | func insert(_ element: Element, at index: Int, completion: (() -> Void)? = nil) { 235 | queue.async(flags: .barrier) { 236 | self.array.insert(element, at: index) 237 | DispatchQueue.main.async { completion?() } 238 | } 239 | } 240 | 241 | /// Removes and returns the first element of the collection. 242 | /// 243 | /// The collection must not be empty. 244 | /// The task is performed asynchronously due to thread-locking management. 245 | /// 246 | /// - Parameter completion: The handler with the removed element. 247 | func removeFirst(completion: ((Element) -> Void)? = nil) { 248 | queue.async(flags: .barrier) { 249 | let element = self.array.removeFirst() 250 | DispatchQueue.main.async { completion?(element) } 251 | } 252 | } 253 | 254 | /// Removes the specified number of elements from the beginning of the collection. 255 | /// 256 | /// The task is performed asynchronously due to thread-locking management. 257 | /// 258 | /// - Parameters: 259 | /// - k: The number of elements to remove from the collection. 260 | /// - completion: The block to execute when remove completed. 261 | func removeFirst(_ k: Int, completion: (() -> Void)? = nil) { 262 | queue.async(flags: .barrier) { 263 | defer { DispatchQueue.main.async { completion?() } } 264 | guard 0...self.array.count ~= k else { return } 265 | self.array.removeFirst(k) 266 | } 267 | } 268 | 269 | /// Removes and returns the last element of the collection. 270 | /// 271 | /// The collection must not be empty. 272 | /// The task is performed asynchronously due to thread-locking management. 273 | /// 274 | /// - Parameter completion: The handler with the removed element. 275 | func removeLast(completion: ((Element) -> Void)? = nil) { 276 | queue.async(flags: .barrier) { 277 | let element = self.array.removeLast() 278 | DispatchQueue.main.async { completion?(element) } 279 | } 280 | } 281 | 282 | /// Removes the specified number of elements from the end of the collection. 283 | /// 284 | /// The task is performed asynchronously due to thread-locking management. 285 | /// 286 | /// - Parameters: 287 | /// - k: The number of elements to remove from the collection. 288 | /// - completion: The block to execute when remove completed. 289 | func removeLast(_ k: Int, completion: (() -> Void)? = nil) { 290 | queue.async(flags: .barrier) { 291 | defer { DispatchQueue.main.async { completion?() } } 292 | guard 0...self.array.count ~= k else { return } 293 | self.array.removeLast(k) 294 | } 295 | } 296 | 297 | /// Removes and returns the element at the specified position. 298 | /// 299 | /// The task is performed asynchronously due to thread-locking management. 300 | /// 301 | /// - Parameters: 302 | /// - index: The position of the element to remove. 303 | /// - completion: The handler with the removed element. 304 | func remove(at index: Int, completion: ((Element) -> Void)? = nil) { 305 | queue.async(flags: .barrier) { 306 | let element = self.array.remove(at: index) 307 | DispatchQueue.main.async { completion?(element) } 308 | } 309 | } 310 | 311 | /// Removes and returns the elements that meet the criteria. 312 | /// 313 | /// The task is performed asynchronously due to thread-locking management. 314 | /// 315 | /// - Parameters: 316 | /// - predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match. 317 | /// - completion: The handler with the removed elements. 318 | func remove(where predicate: @escaping (Element) -> Bool, completion: (([Element]) -> Void)? = nil) { 319 | queue.async(flags: .barrier) { 320 | var elements = [Element]() 321 | 322 | while let index = self.array.firstIndex(where: predicate) { 323 | elements.append(self.array.remove(at: index)) 324 | } 325 | 326 | DispatchQueue.main.async { completion?(elements) } 327 | } 328 | } 329 | 330 | /// Removes all elements from the array. 331 | /// 332 | /// The task is performed asynchronously due to thread-locking management. 333 | /// 334 | /// - Parameter completion: The handler with the removed elements. 335 | func removeAll(completion: (([Element]) -> Void)? = nil) { 336 | queue.async(flags: .barrier) { 337 | let elements = self.array 338 | self.array.removeAll() 339 | DispatchQueue.main.async { completion?(elements) } 340 | } 341 | } 342 | } 343 | 344 | public extension SynchronizedArray { 345 | 346 | /// Accesses the element at the specified position if it exists. 347 | /// 348 | /// - Parameter index: The position of the element to access. 349 | /// - Returns: optional element if it exists. 350 | subscript(index: Int) -> Element? { 351 | get { 352 | var result: Element? 353 | queue.sync { result = self.array[safe: index] } 354 | return result 355 | } 356 | 357 | set { 358 | guard let newValue = newValue else { return } 359 | 360 | queue.async(flags: .barrier) { 361 | self.array[index] = newValue 362 | } 363 | } 364 | } 365 | } 366 | 367 | // MARK: - Equatable 368 | 369 | public extension SynchronizedArray where Element: Equatable { 370 | 371 | /// Returns a Boolean value indicating whether the sequence contains the given element. 372 | /// 373 | /// - Parameter element: The element to find in the sequence. 374 | /// - Returns: true if the element was found in the sequence; otherwise, false. 375 | func contains(_ element: Element) -> Bool { 376 | var result = false 377 | queue.sync { result = self.array.contains(element) } 378 | return result 379 | } 380 | 381 | /// Removes the specified element. 382 | /// 383 | /// The task is performed asynchronously due to thread-locking management. 384 | /// 385 | /// - Parameter element: An element to search for in the collection. 386 | func remove(_ element: Element, completion: (() -> Void)? = nil) { 387 | queue.async(flags: .barrier) { 388 | self.array.remove(element) 389 | DispatchQueue.main.async { completion?() } 390 | } 391 | } 392 | 393 | /// Removes the specified element. 394 | /// 395 | /// The task is performed asynchronously due to thread-locking management. 396 | /// 397 | /// - Parameters: 398 | /// - left: The collection to remove from. 399 | /// - right: An element to search for in the collection. 400 | static func -= (left: inout SynchronizedArray, right: Element) { 401 | left.remove(right) 402 | } 403 | } 404 | 405 | // MARK: - Infix operators 406 | 407 | public extension SynchronizedArray { 408 | 409 | /// Adds a new element at the end of the array. 410 | /// 411 | /// The task is performed asynchronously due to thread-locking management. 412 | /// 413 | /// - Parameters: 414 | /// - left: The collection to append to. 415 | /// - right: The element to append to the array. 416 | static func += (left: inout SynchronizedArray, right: Element) { 417 | left.append(right) 418 | } 419 | 420 | /// Adds new elements at the end of the array. 421 | /// 422 | /// The task is performed asynchronously due to thread-locking management. 423 | /// 424 | /// - Parameters: 425 | /// - left: The collection to append to. 426 | /// - right: The elements to append to the array. 427 | static func += (left: inout SynchronizedArray, right: [Element]) { 428 | left.append(right) 429 | } 430 | } 431 | 432 | private extension SynchronizedArray { 433 | //swiftlint:disable file_length 434 | } 435 | -------------------------------------------------------------------------------- /Sources/Prefpane Sources/View Controllers/ControllersRef.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return., Gregorio Litenstein. 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 10 | import AppKit 11 | 12 | /** Utilitary Singleton containing references to the proper instances of View controllers, tabs, taba data and so on. */ 13 | final internal class ControllersRef: NSObject { 14 | @IBOutlet weak var thePrefPane: SWDAMainPrefPane? 15 | @IBOutlet weak var theTabView: NSTabView! 16 | @IBOutlet weak var theMainView: NSView! 17 | @IBOutlet weak var tabViewController: SWDATabViewController? 18 | 19 | /** Store references to the original font of an NSControl to revert effects of fitWidth() when appropriate. */ 20 | var originalFonts: [NSControl:NSFont] = [:] 21 | 22 | /** Store a reference to the main icon, since we cannot use mainBundle on a preference pane. */ 23 | static let appIcon: NSImage = NSWorkspace.shared.icon(forFile: Bundle(identifier: "cl.fail.lordkamina.SwiftDefaultApps")!.bundlePath) 24 | 25 | /** Information relevant to the different tabs, such as labels, Keypaths for their content arrays, and such. */ 26 | enum TabData { 27 | /** Transform Tab names to abstract cases. */ 28 | enum tabNames:String { 29 | case Internet 30 | case URIs 31 | case UTIs 32 | case Applications 33 | init? (value: String?) { 34 | if let value = value { 35 | switch value { 36 | case "Internet": self = .Internet 37 | case "URI Schemes": self = .URIs 38 | case "Uniform Type Identifiers": self = .UTIs 39 | case "Applications": self = .Applications 40 | default: return nil 41 | } 42 | } 43 | else { return nil } 44 | } 45 | } 46 | 47 | /** Return a tab's label. */ 48 | static var Tab: String? { 49 | let name = tabNames(value:(ControllersRef.sharedInstance.tabViewController?.currentTab?.label)) 50 | return name?.rawValue 51 | } 52 | 53 | /** Store a reference to the appropriate Keypath to an item's Display Name (the title of the Detail View). */ 54 | static var displayNameKeyPath: String { 55 | if let Tab = Tab { 56 | switch Tab { 57 | case "Internet": return "contentDescription" 58 | case "URIs": return "contentName" 59 | case "UTIs": return "contentName" 60 | case "Applications": return "displayName" 61 | default: return "contentName" 62 | } 63 | } 64 | else { return "contentName" } 65 | } 66 | 67 | /** Stores a reference to the appropriate Keypath for each tab's Content Array. */ 68 | static var modelArrayKeyPath: String { 69 | if let Tab = Tab { 70 | var modelArray = "" 71 | 72 | switch Tab { 73 | case "Internet": modelArray = #keyPath(SWDAHandlersModel.internetSchemes) 74 | case "URIs": modelArray = #keyPath(SWDAHandlersModel.allSchemes) 75 | case "UTIs": modelArray = #keyPath(SWDAHandlersModel.allUTIs) 76 | case "Applications": modelArray = #keyPath(SWDAHandlersModel.allApps) 77 | default: modelArray = "" 78 | } 79 | return modelArray 80 | } 81 | else { return "" } 82 | } 83 | /** Return or populate each tab's Content Array. */ 84 | static func getContentArray (for view: SWDATabTemplate, initialSetup: Bool = false) { 85 | 86 | if let Tab = Tab { 87 | let modelArray = (initialSetup == true) ? #keyPath(SWDAHandlersModel.internetSchemes) : modelArrayKeyPath 88 | if (SWDAHandlersModel.value(forKey:modelArray) != nil) { 89 | view.setValue((SWDAHandlersModel.value(forKey:modelArray) as! Array), forKey: "contentArrayStore") 90 | } 91 | else { 92 | SWDAHandlersModel.populateContentArray(in: view, with: Tab) { [unowned view] in 93 | view.setValue((SWDAHandlersModel.value(forKey:modelArray) as! Array), forKey: "contentArrayStore") 94 | } 95 | } 96 | } 97 | else { return } 98 | } 99 | 100 | /** Determines whether to show the Item Description in the Detail View. */ 101 | static var shouldShowDescription: Bool { 102 | if let Tab = Tab { 103 | switch Tab { 104 | case "Internet": return false 105 | case "URIs": return true 106 | case "UTIs": return true 107 | case "Applications": return true 108 | default: return true 109 | } 110 | } 111 | else { return true } 112 | } 113 | 114 | /** Determines whether to show the "Add" button for custom URI Schemes. */ 115 | static var shouldShowAddRemove: Bool { 116 | if let Tab = Tab { 117 | switch Tab { 118 | case "URIs": return true 119 | default: return false 120 | } 121 | } 122 | else { return false } 123 | } 124 | 125 | /** Determines whether to show an Application's path and a Reveal in Finder button. */ 126 | static var shouldShowAppPath: Bool { 127 | if let Tab = Tab { 128 | switch Tab { 129 | case "Applications": return true 130 | default: return false 131 | } 132 | } 133 | else { return false } 134 | } 135 | 136 | /** Determines whether to show a list of file extensions associated with a given UTI. */ 137 | static var shouldShowFileExts: Bool { 138 | if let Tab = Tab { 139 | switch Tab { 140 | case "UTIs": return true 141 | default: return false 142 | } 143 | } 144 | else { return false } 145 | } 146 | } 147 | 148 | /** Initialize the singleton in a thread-safe way. */ 149 | static let sharedInstance: ControllersRef = ControllersRef() 150 | 151 | /** Make sure there are no stray instances. */ 152 | private override init() { 153 | super.init() 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Sources/Prefpane Sources/View Controllers/DryView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DryView.swift 3 | // CocoaBindingDryView-ReusableViews 4 | // 5 | // Created by AMTourky on 6/25/16. 6 | // 7 | // Copyright © 2016 AMTourky. All rights reserved. 8 | // www.amtourky.me 9 | // Modified by Gregorio Litenstein for use on SwiftDefaultApps. 10 | 11 | // Licensed under the Apache License, Version 2.0 (the "License"); 12 | // you may not use this file except in compliance with the License. 13 | // You may obtain a copy of the License at 14 | // 15 | // http://www.apache.org/licenses/LICENSE-2.0 16 | 17 | 18 | import Cocoa 19 | 20 | @IBDesignable class DRYView: NSView { 21 | private let dRYViewKVOContext = UnsafeMutableRawPointer(bitPattern: 1) 22 | @IBOutlet weak var descriptionControl: NSTextField? 23 | @IBOutlet weak var titleControl: NSTextField? 24 | 25 | @IBInspectable var nibName: String? 26 | { 27 | didSet 28 | { 29 | guard let theNibName = self.nibName 30 | else { return } 31 | 32 | var objects: NSArray? = NSArray() 33 | self.bundle?.loadNibNamed(theNibName, owner: self, topLevelObjects: &objects) 34 | if let theObjects = objects 35 | { 36 | for view in theObjects 37 | { 38 | if let theView = view as? NSView 39 | { 40 | self.addSubview(theView) 41 | theView.translatesAutoresizingMaskIntoConstraints = false 42 | self.selfViewConstraintsToBeFollowedByView(theView) 43 | break 44 | } 45 | } 46 | } 47 | } 48 | } 49 | 50 | lazy var bundle: Bundle? = 51 | { 52 | guard let theNibName = self.nibName 53 | else {return nil} 54 | 55 | var objects: NSArray? = NSArray() 56 | var isLoaded = Bundle.main.loadNibNamed(theNibName, owner: self, topLevelObjects: &objects) 57 | if isLoaded 58 | { 59 | return Bundle.main 60 | } 61 | else 62 | { 63 | return Bundle(for: self.classForCoder) 64 | } 65 | }() 66 | 67 | func selfViewConstraintsToBeFollowedByView(_ view: NSView) 68 | { 69 | let widthConstraint = NSLayoutConstraint(item: self, attribute: .width , relatedBy: .equal, toItem: view, attribute: .width , multiplier: 1, constant: 0) 70 | let centerXConstraint = NSLayoutConstraint(item: self, attribute: .centerX , relatedBy: .equal, toItem: view, attribute: .centerX , multiplier: 1, constant: 0) 71 | centerXConstraint.identifier = "DRYView Auto CenterX" 72 | let centerYConstraint = NSLayoutConstraint(item: self, attribute: .centerY , relatedBy: .equal, toItem: view, attribute: .centerY , multiplier: 1, constant: 0) 73 | centerYConstraint.identifier = "DRYView Auto CenterY" 74 | widthConstraint.identifier = "DRYView Auto Width" 75 | let heightConstraint = NSLayoutConstraint(item: self, attribute: .height , relatedBy: .equal, toItem: view, attribute: .height , multiplier: 1, constant: 0) 76 | heightConstraint.identifier = "DRYView Auto Height" 77 | self.addConstraints([centerXConstraint, centerYConstraint, widthConstraint, heightConstraint]) 78 | } 79 | 80 | 81 | 82 | @objc var inspectedObject: NSObject? { 83 | didSet { 84 | self.willChangeValue(forKey: "inspectedObject") 85 | self.didChangeValue(forKey: "inspectedObject") 86 | 87 | self.titleControl?.fitWidth() 88 | self.descriptionControl?.fitWidth() 89 | } 90 | } 91 | 92 | 93 | @IBOutlet var inspectedObjectControllerReference: NSObjectController? { 94 | didSet { 95 | self.inspectedObject = self.inspectedObjectControllerReference?.content as? NSObject 96 | addObserver(self, forKeyPath: #keyPath(inspectedObjectControllerReference.content), options: .new, context: dRYViewKVOContext) 97 | } 98 | } 99 | 100 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 101 | guard (dRYViewKVOContext == context) else { 102 | super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) 103 | return 104 | } 105 | if (dRYViewKVOContext == context) { 106 | self.inspectedObject = self.inspectedObjectControllerReference?.content as? NSObject 107 | } 108 | } 109 | 110 | deinit 111 | { 112 | if let _ = inspectedObject { 113 | removeObserver(self, forKeyPath: #keyPath(inspectedObjectControllerReference.content), context: dRYViewKVOContext) 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Sources/Prefpane Sources/View Controllers/ProgressAlert.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return., Gregorio Litenstein. 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 10 | import AppKit 11 | 12 | /** Create a progressbar and display it on a modal sheet while the Content Array is populated asynchronously. */ 13 | class ProgressAlert: NSAlert { 14 | var progressBar = NSProgressIndicator() 15 | override init() { 16 | progressBar.isIndeterminate = false 17 | progressBar.style = .bar 18 | super.init() 19 | 20 | self.messageText = "" 21 | self.informativeText = "Loading..." 22 | self.icon = ControllersRef.appIcon 23 | 24 | self.accessoryView = NSView(frame: NSRect(x:0, y:0, width: 290, height: 16)) 25 | self.accessoryView?.addSubview(progressBar) 26 | self.layout() 27 | self.accessoryView?.setFrameOrigin(NSPoint(x:(self.accessoryView?.frame)!.minX,y:self.window.frame.maxY)) 28 | 29 | self.addButton(withTitle: "") 30 | progressBar.sizeToFit() 31 | progressBar.setFrameSize(NSSize(width:290, height: 16)) 32 | progressBar.usesThreadedAnimation = true 33 | } 34 | 35 | func increment(by value:Double) { 36 | progressBar.increment(by: value) 37 | } 38 | var maxValue: Double { 39 | get { 40 | return progressBar.maxValue 41 | } 42 | set { 43 | progressBar.maxValue = newValue 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/Prefpane Sources/View Controllers/SWDATreeRow.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return., Gregorio Litenstein. 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 10 | import AppKit 11 | 12 | /** Represent an instance of a kind of Content (UTI/URL) and a valid associated application in a given role. */ 13 | class SWDAContentHandler: NSObject { 14 | var content: NSObjectProtocol & SWDAContentProtocol 15 | var appName: String = "None" 16 | 17 | var application: SWDAApplicationInfo? { 18 | if let info = SWDAApplicationInfo(self.appName) { 19 | return info 20 | } 21 | else { return nil } 22 | } 23 | var roleMask: SourceListRoleTypes? 24 | init(_ content: NSObjectProtocol & SWDAContentProtocol, appName: String, role: SourceListRoleTypes?) { 25 | self.content = content 26 | self.appName = appName 27 | if let role = role { 28 | self.roleMask = role 29 | } 30 | super.init() 31 | } 32 | } 33 | 34 | /** Our NSObject sub-class that represents a ContentHandler, foundation of the Detail Outline view and as such responsible for most of the heavy lifting */ 35 | internal class SWDATreeRow:NSObject { 36 | 37 | /** Used chiefly for cosmetic purposes in presenting and styling the rows. */ 38 | enum levels { 39 | case header, role, handler 40 | } 41 | 42 | /** Required functionality for our NSTreeView. */ 43 | weak var parentNode: SWDATreeRow? 44 | @objc var children: [SWDATreeRow] = [] 45 | @objc var count: Int { return children.count } 46 | @objc var isLeaf: Bool { return children.count < 1} 47 | 48 | 49 | /** Dirty trick to avoid subclassing NSOutlineView. */ 50 | @objc var shouldFauxIndent: Bool { 51 | return self.rowLevel == .handler 52 | } 53 | 54 | /** Actually determine what kind of row we are. */ 55 | var rowLevel: levels { 56 | if (self.parentNode == nil) { return .header } 57 | else { 58 | if (self.children.count < 1) { return .handler } 59 | else { return .role } 60 | } 61 | } 62 | 63 | /** Reference to the System Font; not strictly needed but in case it changes in the future, this way we only actually need to modify it in one place. */ 64 | var baseFont: NSFont = NSFont.systemFont(ofSize:0) 65 | 66 | /** Bindings-compatible determination of the font to use. */ 67 | @objc var rowFont: NSFont? { 68 | switch self.rowLevel { 69 | case .header: return self.baseFont.smallCaps() 70 | case .role: return self.baseFont.bold() 71 | default: return self.baseFont 72 | } 73 | } 74 | /** Show header rows in a different color. */ 75 | @objc var textColor: NSColor? { 76 | switch self.rowLevel { 77 | case .header: return NSColor.headerColor 78 | default: return NSColor.controlTextColor 79 | } 80 | } 81 | 82 | /** What the Tree Row will actually display as a label. */ 83 | @objc var rowTitle: String 84 | 85 | /** Content and associated application. Optional to account for dummy rows like Headers. */ 86 | var rowContent: SWDAContentHandler? 87 | var roleMask: SourceListRoleTypes? 88 | 89 | @objc lazy var appIcon: NSImage? = { return self.rowContent?.application?.appIcon }() 90 | 91 | /** Binding-compatible determination used exclusively for SWDATreeRows in the Applications tab. Returns true if the currently selected application is the default handler for the content represented by this row. It's only enabled when its state is off, since in practice it's not actually possible to remove handlers from LaunchServices. Rather, the service takes care of its own clean-up if it detects an UTI or URL Scheme does not have any valid handlers. */ 92 | @objc var isHandlingContent: Bool { 93 | get { 94 | guard (ControllersRef.sharedInstance.tabViewController!.currentTab?.label == "Applications") else { return false } 95 | guard (self.rowContent != nil) else { return false } 96 | let contentType = self.rowContent?.content.contentType 97 | guard (contentType != .Application) else { return false } 98 | let defaultHandler = (contentType == .URI) ? LSWrappers.Schemes.copyDefaultHandler(self.rowTitle, asPath: false) : LSWrappers.UTType.copyDefaultHandler(self.rowTitle, inRoles: LSRolesMask(from:self.roleMask!), asPath: false) 99 | return self.rowContent?.application?.appBundleID?.lowercased() == defaultHandler?.lowercased() 100 | } 101 | set { 102 | guard (ControllersRef.sharedInstance.tabViewController!.currentTab?.label == "Applications") else { return } 103 | if let content = (self.rowContent?.content as? SWDAContentItem) { 104 | let contentName = content.contentName 105 | let type = content.contentType 106 | var status = OSStatus() 107 | 108 | if let bundleID = self.rowContent?.application?.appBundleID { 109 | 110 | status = (type == .URI) ? LSWrappers.Schemes.setDefaultHandler(contentName, bundleID) : LSWrappers.UTType.setDefaultHandler(contentName, bundleID, LSRolesMask(from:self.roleMask!)) 111 | try! displayAlert(error: status, arg1: (self.rowContent?.application?.displayName), arg2: self.rowTitle) 112 | if let parent = self.parentNode { 113 | for node in parent.children { 114 | node.willChangeValue(forKey: "isDefaultHandler") 115 | node.didChangeValue(forKey: "isDefaultHandler") 116 | } 117 | } 118 | } 119 | } 120 | } 121 | } 122 | 123 | /** Binding-compatible determination used in every tab except for Applications. Returns true if the application represented by the current row is the default handler for the selected content type. Allows "Other" to specify apps that aren't detected as valid handlers for whatever reason. Note that setting something to be handled by "Other" will not _actually_ work unless the application's Info.plist adequately declares association with that content. */ 124 | @objc var isDefaultHandler: Bool { 125 | get { 126 | if let content = (self.rowContent?.content as? SWDAContentItem) { 127 | var handler: String? 128 | switch content.contentType { 129 | case .Application: return false 130 | case .UTI: handler = LSWrappers.UTType.copyDefaultHandler(content.contentName, inRoles:LSRolesMask(from:self.roleMask!), asPath: false) 131 | case .URI: handler = LSWrappers.Schemes.copyDefaultHandler(content.contentName, asPath: false) 132 | } 133 | guard (handler != nil) else { return false } 134 | return (handler?.lowercased() == rowContent?.application?.appBundleID?.lowercased()) 135 | } 136 | else { return false } 137 | } 138 | set { 139 | if let content = (self.rowContent?.content as? SWDAContentItem) { 140 | let contentName = content.contentName 141 | let type = content.contentType 142 | var status = OSStatus() 143 | if let bundleID = self.rowContent?.application?.appBundleID { 144 | status = (type == .URI) ? LSWrappers.Schemes.setDefaultHandler(contentName, bundleID) : LSWrappers.UTType.setDefaultHandler(contentName, bundleID, LSRolesMask(from:self.roleMask!)) 145 | try! displayAlert(error: status, arg1: (self.rowContent?.application?.displayName), arg2: self.rowTitle) 146 | if let parent = self.parentNode { 147 | for node in parent.children { 148 | node.willChangeValue(forKey: "isDefaultHandler") 149 | node.didChangeValue(forKey: "isDefaultHandler") 150 | } 151 | } 152 | } 153 | else if (self.rowTitle == "Other...") { 154 | let openpanel = NSOpenPanel() 155 | openpanel.treatsFilePackagesAsDirectories = false 156 | openpanel.allowsMultipleSelection = false 157 | openpanel.canChooseDirectories = false 158 | openpanel.resolvesAliases = true 159 | openpanel.canChooseFiles = true 160 | openpanel.allowedFileTypes = ["app"] 161 | openpanel.allowsOtherFileTypes = true 162 | openpanel.title = "Choose a default application for: \(content.displayName)" 163 | openpanel.prompt = "Add" 164 | openpanel.runModal() 165 | if !openpanel.urls.isEmpty { 166 | let handler = SWDAContentHandler(content, appName:openpanel.urls[0].path, role:self.roleMask) 167 | if let tempName = handler.application?.displayName { 168 | let displayName = (tempName.lowercased().range(of:".app") != nil) ? tempName : "\(tempName).app" 169 | let row = SWDATreeRow(displayName, content: handler) 170 | self.parentNode?.addChild(row) 171 | row.isDefaultHandler = true 172 | } 173 | else { 174 | let displayName = FileManager.default.displayName(atPath: openpanel.urls[0].path) 175 | let row = SWDATreeRow(displayName, content: handler) 176 | self.parentNode?.addChild(row) 177 | row.isDefaultHandler = true 178 | } 179 | self.parentNode?.children.sort(){($0.rowTitle < $1.rowTitle) && ($0.rowTitle != "Other..." && $0.rowTitle != "Do Nothing")} 180 | 181 | self.parentNode?.willChangeValue(forKey: "children") 182 | self.parentNode?.didChangeValue(forKey: "children") 183 | } 184 | } 185 | } 186 | } 187 | } 188 | 189 | /** 190 | Add a Child Row 191 | - Parameter child: An instance of SWDATreeRow to be added as a child. 192 | */ 193 | func addChild (_ child: SWDATreeRow) { 194 | guard (self.children.firstIndex(of: child) == nil) else { return } 195 | child.parentNode = self 196 | self.children.append(child) 197 | } 198 | 199 | /** 200 | Add all the children of a given SWDATreeRow as children of this instance. 201 | - Parameter row: An instance of SWDATreeRow to take children from. 202 | - Note: This is only used to hide the "Viewer" role from URL Schemes, since handler roles are not currently implemented for URL Handlers. 203 | */ 204 | func addChildren (of row: SWDATreeRow) { 205 | for child in row.children { 206 | guard (self.children.firstIndex(of: child) == nil) else { return } 207 | child.parentNode = self 208 | self.children.append(child) 209 | } 210 | } 211 | 212 | init(_ name: String, content: SWDAContentHandler? = nil) { 213 | self.rowTitle = name 214 | if let content = content { 215 | self.rowContent = content 216 | self.roleMask = content.roleMask 217 | } 218 | super.init() 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /Sources/Prefpane Sources/View Controllers/Subclasses.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return., Gregorio Litenstein. 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 10 | import AppKit 11 | 12 | extension DRYView { 13 | /** Bridge required to make currentTab really dependent on the selectedTabViewItemIndex */ 14 | @objc weak var tabViewController: SWDATabViewController? { return ControllersRef.sharedInstance.tabViewController } 15 | 16 | /** Reference to the currently selected TabViewItem */ 17 | @objc dynamic var currentTab: String? { 18 | if let selectedTab = ControllersRef.sharedInstance.tabViewController?.currentTab { 19 | return selectedTab.label 20 | } 21 | else { return nil } 22 | } 23 | 24 | /** Open Finder and Reveal the currently selected Application. */ 25 | @IBAction func revealAppInFinder(_ sender: NSButton) { 26 | NSWorkspace.shared.activateFileViewerSelecting([URL(fileURLWithPath:sender.title)]) 27 | } 28 | 29 | /** Determines whether to show a Description in the Detail View */ 30 | @objc var showDescriptionBool: NSNumber { 31 | guard (self.currentTab != nil) else { return NSNumber(booleanLiteral:true) } 32 | return NSNumber(booleanLiteral:ControllersRef.TabData.shouldShowDescription) 33 | } 34 | 35 | /** Determines whether to show a path and Reveal In Finder button */ 36 | @objc var showPathBool: NSNumber { 37 | guard (self.currentTab != nil) else { return NSNumber(booleanLiteral:false) } 38 | return NSNumber(booleanLiteral:ControllersRef.TabData.shouldShowAppPath) 39 | } 40 | 41 | /** The only tab where an "Add" button is at all meaningful is the URI Schemes. */ 42 | @objc var showAddRemoveBool: NSNumber { 43 | guard (self.currentTab != nil) else { return NSNumber(booleanLiteral:false) } 44 | return NSNumber(booleanLiteral:ControllersRef.TabData.shouldShowAddRemove) 45 | } 46 | 47 | /** Determines whether to ahow a list of file extensions associated with a given UTI. */ 48 | @objc var showFileExtensionsBool: NSNumber { 49 | guard (self.currentTab != nil) else { return NSNumber(booleanLiteral:false) } 50 | return NSNumber(booleanLiteral:ControllersRef.TabData.shouldShowFileExts) 51 | } 52 | 53 | @objc class func keyPathsForValuesAffectingShowDescriptionBool() -> Set { 54 | return Set([#keyPath(currentTab), #keyPath(inspectedObject)]) 55 | } 56 | @objc class func keyPathsForValuesAffectingShowPathBool() -> Set { 57 | return Set([#keyPath(currentTab), #keyPath(inspectedObject)]) 58 | } 59 | @objc class func keyPathsForValuesAffectingShowAddRemoveBool() -> Set { 60 | return Set([#keyPath(currentTab), #keyPath(inspectedObject)]) 61 | } 62 | @objc class func keyPathsForValuesAffectingShowFileExtensionsBool() -> Set { 63 | return Set([#keyPath(currentTab), #keyPath(inspectedObject)]) 64 | } 65 | @objc class func keyPathsForValuesAffectingCurrentTab() -> Set { 66 | return Set(["tabViewController.selectedTabViewItemIndex"]) 67 | } 68 | } 69 | 70 | /** NSView subclass used to implement automatic sorting of the NSTableView. */ 71 | class SWDATableView: NSView { 72 | 73 | @IBOutlet weak var arrayController: NSArrayController? 74 | @IBOutlet weak var tableView: NSTableView? 75 | 76 | let defaultSortDescriptors = [NSSortDescriptor(key: "displayName", ascending: true, selector: #selector(NSString.caseInsensitiveCompare(_:)))] 77 | override func awakeFromNib() { 78 | if let ac = arrayController { 79 | if let tableView = tableView { 80 | ac.bind(NSBindingName(rawValue: "sortDescriptors"), to:tableView, withKeyPath: "sortDescriptors", options:nil) 81 | tableView.sortDescriptors = defaultSortDescriptors 82 | tableView.selectRowIndexes(NSIndexSet(index: 0) as IndexSet, byExtendingSelection: false) 83 | self.tableView?.becomeFirstResponder() 84 | } 85 | } 86 | } 87 | } 88 | 89 | /** By subclassing (instead of extending) DRYView for this, we make sure variables such as the contentArray and other possibly-work-intensive code is only executed on instances of the tab and not also in the detail view.*/ 90 | class SWDATabTemplate: DRYView { 91 | @IBOutlet weak var progressAlert: ProgressAlert? 92 | @IBOutlet weak var titleView: NSView? 93 | @IBOutlet weak var customNewScheme: NSTextField? 94 | @IBOutlet var arrayController: NSArrayController! 95 | 96 | @IBOutlet var tableView: NSTableView! 97 | 98 | /** Let's save selected item for each tab, update selectionIndex with the current value of our selection when our selection would be replaced with an empty or invalid one. */ 99 | @objc var tableIndexes = NSIndexSet(index: 0) 100 | { 101 | willSet { 102 | if (newValue.count < 1) { self.selectionIndex = self.tableIndexes.firstIndex } 103 | } 104 | } 105 | 106 | /** Add a custom URI Scheme and assign our dummy app as the default handler. In practice this should almost never be necessary but sometimes Launch Services move in mysterious ways. */ 107 | @IBAction func addCustomScheme(_ sender: NSButton) { 108 | guard (customNewScheme?.stringValue != nil) && (customNewScheme?.stringValue != "") else { return } 109 | let result = LSWrappers.Schemes.setDefaultHandler(customNewScheme!.stringValue, "cl.fail.lordkamina.ThisAppDoesNothing") 110 | if (result == 0) { 111 | SWDAHandlersModel.setValue(nil, forKey: "allSchemes") 112 | self.setValue(nil, forKey: "contentArrayStore") 113 | } 114 | try! displayAlert(error: result, arg1: "Do Nothing", arg2: customNewScheme!.stringValue) 115 | return 116 | } 117 | 118 | /** Identifies which tab the current instance belongs to. */ 119 | var tabIndex: Int? = -1 120 | 121 | /** This is where we'll store our selection indexes when switching tabs. */ 122 | var selectionIndex: Int? = nil 123 | 124 | /** Backing Store for the list of items on each tab. */ 125 | @objc var contentArrayStore: Array? 126 | 127 | /** Check whether contentArrayStore holds anything; return that if it does or populate it asynchronously if it doesn't. */ 128 | @objc var contentArray: Array? { 129 | guard self.nibName == "SWDAPrefpaneTabTemplate" else { return [] } 130 | if (self.tabIndex == ControllersRef.sharedInstance.tabViewController?.selectedTabViewItemIndex) { 131 | if (self.contentArrayStore == nil) { 132 | ControllersRef.TabData.getContentArray(for: self) 133 | return nil 134 | } 135 | else { 136 | if let content = self.value(forKey:"contentArrayStore") as? Array { 137 | defer { self.tableView?.becomeFirstResponder() } 138 | return content 139 | } 140 | else { return nil } 141 | } 142 | } 143 | else { 144 | return nil 145 | } 146 | } 147 | 148 | @objc class func keyPathsForValuesAffectingContentArray() -> Set { 149 | return Set([#keyPath(currentTab), #keyPath(contentArrayStore)]) 150 | } 151 | 152 | @objc class func keyPathsForValuesAffectingContentArrayStore() -> Set { 153 | return Set([#keyPath(currentTab)]) 154 | } 155 | } 156 | 157 | /** NSTabView subclass, initializes instances of the canned tabs, builds the tabView and handles saving data such as selection indexes between multiple tabs. */ 158 | class SWDATabViewController: NSTabViewController { 159 | /** KVO context */ 160 | private let tabViewKVOContext = UnsafeMutableRawPointer(bitPattern: 1) 161 | 162 | /** Return an instance of the currently selected NSTabViewItem */ 163 | var currentTab: NSTabViewItem? { 164 | get { 165 | guard self.isViewLoaded != false else { return nil } 166 | let index = self.selectedTabViewItemIndex 167 | guard index != -1 else { return nil } 168 | let tab = self.tabView.tabViewItem(at: index) 169 | return tab 170 | } 171 | } 172 | 173 | /** Preserve selection indexes when switching tabs. */ 174 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 175 | guard (context == tabViewKVOContext) else { 176 | super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) 177 | return 178 | } 179 | if (tabViewKVOContext == context) { 180 | guard (change![.oldKey]! as! Int != -1) else { return } 181 | let oldTab = self.children[change![.oldKey]! as! Int].view as! SWDATabTemplate 182 | let newTab = self.children[change![.newKey]! as! Int].view as! SWDATabTemplate 183 | 184 | if (newTab.selectionIndex != 0) { 185 | newTab.setValue(NSIndexSet(index: newTab.selectionIndex!), forKey: "tableIndexes") 186 | } 187 | else { 188 | newTab.setValue(NSIndexSet(index:0), forKey: "tableIndexes") 189 | } 190 | newTab.tableView?.scrollRowToVisible(newTab.arrayController.selectionIndex) 191 | } 192 | } 193 | /** Initialize the NSTabView, create the tabs and assign their NIB. */ 194 | override func viewDidLoad() { 195 | self.tabView.tabViewType = .topTabsBezelBorder 196 | addObserver(self, forKeyPath: #keyPath(selectedTabViewItemIndex), options: [.old, .new], context: tabViewKVOContext) 197 | let tabViewController = self 198 | self.tabView.translatesAutoresizingMaskIntoConstraints = false 199 | let leading = NSLayoutConstraint(item: ControllersRef.sharedInstance.theMainView!, attribute: .leading , relatedBy: .equal, toItem: self.tabView, attribute: .leading , multiplier: 1, constant: -20) 200 | leading.identifier = "TabView Leading" 201 | let trailing = NSLayoutConstraint(item: ControllersRef.sharedInstance.theMainView!, attribute: .trailing , relatedBy: .equal, toItem: self.tabView, attribute: .trailing , multiplier: 1, constant: 20) 202 | trailing.identifier = "TabView Trailng" 203 | let centerX = NSLayoutConstraint(item: ControllersRef.sharedInstance.theMainView!, attribute: .centerX , relatedBy: .equal, toItem: self.tabView, attribute: .centerX , multiplier: 1, constant: 0) 204 | centerX.identifier = "TabView CenterX" 205 | let top = NSLayoutConstraint(item: ControllersRef.sharedInstance.theMainView!, attribute: .top , relatedBy: .equal, toItem: self.tabView, attribute: .top , multiplier: 1, constant: -20) 206 | top.identifier = "TabView Top" 207 | let bottom = NSLayoutConstraint(item: ControllersRef.sharedInstance.theMainView!, attribute: .bottom , relatedBy: .equal, toItem: self.tabView, attribute: .bottom , multiplier: 1, constant: 10) 208 | bottom.identifier = "TabView Bottom" 209 | let tabs = ["Internet", "URI Schemes", "Uniform Type Identifiers", "Applications"] 210 | 211 | ControllersRef.sharedInstance.theMainView.addConstraints([centerX, leading, trailing, bottom, top]) 212 | 213 | for tab in tabs { 214 | let newTabVC = NSViewController.init() 215 | newTabVC.view = SWDATabTemplate.init() 216 | let index = tabs.firstIndex(of: tab) 217 | (newTabVC.view as! SWDATabTemplate).tabIndex = (index != nil) ? index! : nil 218 | (newTabVC.view as! SWDATabTemplate).nibName = "SWDAPrefpaneTabTemplate" 219 | 220 | tabViewController.addChild(newTabVC) 221 | 222 | let newTab = tabViewController.tabViewItem(for: newTabVC)! 223 | newTab.label = tab 224 | } 225 | } 226 | 227 | class func keyPathsForValuesAffectingCurrentTab() -> Set { 228 | return Set([#keyPath(selectedTabViewItemIndex)]) 229 | } 230 | 231 | deinit { 232 | if let _ = tabViewKVOContext { 233 | removeObserver(self, forKeyPath: #keyPath(selectedTabViewItemIndex), context: tabViewKVOContext) 234 | } 235 | } 236 | } 237 | 238 | /** 239 | NSTreeController sub-class that acts mostly as a bridge between the DRYViews and a couple Bindings. 240 | Single outlineView delegate function implemented obeying strictly to a cosmetic issue. 241 | */ 242 | class SWDATreeController: NSTreeController, NSOutlineViewDelegate { 243 | required init?(coder: NSCoder) { 244 | super.init(coder: coder) 245 | } 246 | @IBOutlet weak var outlineView: NSOutlineView? 247 | @IBOutlet weak var dryView: DRYView? 248 | 249 | /** 250 | Return true if "Applications" is the currently selected tab. 251 | */ 252 | @objc var showPathBool: NSNumber { 253 | if let view = dryView { 254 | return view.value(forKey:"showPathBool") as! NSNumber 255 | } 256 | else { return NSNumber(booleanLiteral: false) } 257 | } 258 | /** Expand tree items by default. 259 | */ 260 | override var content: Any? { 261 | didSet { 262 | self.outlineView?.expandItem(nil, expandChildren: true) 263 | } 264 | } 265 | /** 266 | Determines whether to show the disclosure button for a group row depending on the selected tab and its place in the hierarchy. 267 | */ 268 | func outlineView(_ outlineView: NSOutlineView, shouldShowOutlineCellForItem item: Any) -> Bool { 269 | let row = item as! NSTreeNode 270 | if let object = row.representedObject as? SWDATreeRow { 271 | if object.rowLevel == .role { return false } 272 | else { 273 | if (object.rowLevel == .header) { 274 | if let isItRole = SourceListRoleTypes(rawValue: object.rowTitle) { 275 | return false 276 | } 277 | else { return true } 278 | } 279 | if let children = row.children { 280 | return children.count > 0 281 | } 282 | else { return false } 283 | } 284 | } 285 | else { return false } 286 | } 287 | } 288 | 289 | @IBDesignable 290 | class HyperlinkTextField: NSTextField { 291 | @IBInspectable var href: String = "" 292 | override func awakeFromNib() { 293 | super.awakeFromNib() 294 | let attributes: [NSAttributedString.Key:AnyObject] = [ 295 | NSAttributedString.Key.foregroundColor: NSColor.blue, 296 | NSAttributedString.Key.backgroundColor: NSColor.clear, 297 | NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue as AnyObject 298 | ] 299 | self.attributedStringValue = NSAttributedString(string: self.stringValue, attributes: attributes) 300 | } 301 | 302 | override func mouseDown(with event: NSEvent) { 303 | NSWorkspace.shared.open(URL(string: self.href)!) 304 | } 305 | convenience init (frame: NSRect, url: String, text: String? = nil) { 306 | self.init(frame:frame) 307 | self.drawsBackground = false 308 | self.isEditable = false 309 | self.isBezeled = false 310 | if let text = text { 311 | self.stringValue = text 312 | } 313 | else { self.stringValue = url } 314 | self.href = url 315 | self.awakeFromNib() 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /Sources/Prefpane Sources/Views/SWDAPrefpaneMain.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 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Sources/Prefpane Sources/Views/SWDAPrefpaneTabTemplate.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 | 105 | 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 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | NSNegateBoolean 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 | YnBsaXN0MDDUAQIDBAUGPT5YJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoK4HCBMU 220 | GR4fIyQrLjE3OlUkbnVsbNUJCgsMDQ4PEBESVk5TU2l6ZVYkY2xhc3NcTlNJbWFnZUZsYWdzVk5TUmVw 221 | c1dOU0NvbG9ygAKADRIgwwAAgAOAC1Z7MSwgMX3SFQoWGFpOUy5vYmplY3RzoReABIAK0hUKGh2iGxyA 222 | BYAGgAkQANIgCiEiXxAUTlNUSUZGUmVwcmVzZW50YXRpb26AB4AITxEIrE1NACoAAAAKAAAADgEAAAMA 223 | AAABAAEAAAEBAAMAAAABAAEAAAECAAMAAAACAAgACAEDAAMAAAABAAEAAAEGAAMAAAABAAEAAAERAAQA 224 | AAABAAAACAESAAMAAAABAAEAAAEVAAMAAAABAAIAAAEWAAMAAAABAAEAAAEXAAQAAAABAAAAAgEcAAMA 225 | AAABAAEAAAFSAAMAAAABAAEAAAFTAAMAAAACAAEAAYdzAAcAAAf0AAAAuAAAAAAAAAf0YXBwbAIgAABt 226 | bnRyR1JBWVhZWiAH0AACAA4ADAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYA 227 | AQAAAADTLWFwcGwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVk 228 | ZXNjAAAAwAAAAG9kc2NtAAABMAAABmZjcHJ0AAAHmAAAADh3dHB0AAAH0AAAABRrVFJDAAAH5AAAAA5k 229 | ZXNjAAAAAAAAABVHZW5lcmljIEdyYXkgUHJvZmlsZQAAAAAAAAAAAAAAFUdlbmVyaWMgR3JheSBQcm9m 230 | aWxlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbWx1YwAAAAAA 231 | AAAfAAAADHNrU0sAAAAqAAABhGVuVVMAAAAoAAABrmNhRVMAAAAsAAAB1nZpVk4AAAAsAAACAnB0QlIA 232 | AAAqAAACLnVrVUEAAAAsAAACWGZyRlUAAAAqAAAChGh1SFUAAAAuAAACrnpoVFcAAAAQAAAC3G5iTk8A 233 | AAAsAAAC7GtvS1IAAAAYAAADGGNzQ1oAAAAkAAADMGhlSUwAAAAgAAADVHJvUk8AAAAkAAADdGRlREUA 234 | AAA6AAADmGl0SVQAAAAuAAAD0nN2U0UAAAAuAAAEAHpoQ04AAAAQAAAELmphSlAAAAAWAAAEPmVsR1IA 235 | AAAkAAAEVHB0UE8AAAA4AAAEeG5sTkwAAAAqAAAEsGVzRVMAAAAoAAAE2nRoVEgAAAAkAAAFAnRyVFIA 236 | AAAiAAAFJmZpRkkAAAAsAAAFSGhySFIAAAA6AAAFdHBsUEwAAAA2AAAFrnJ1UlUAAAAmAAAF5GFyRUcA 237 | AAAoAAAGCmRhREsAAAA0AAAGMgBWAWEAZQBvAGIAZQBjAG4A/QAgAHMAaQB2AP0AIABwAHIAbwBmAGkA 238 | bABHAGUAbgBlAHIAaQBjACAARwByAGEAeQAgAFAAcgBvAGYAaQBsAGUAUABlAHIAZgBpAGwAIABkAGUA 239 | IABnAHIAaQBzACAAZwBlAG4A6AByAGkAYwBDHqUAdQAgAGgA7ABuAGgAIABNAOAAdQAgAHgA4QBtACAA 240 | QwBoAHUAbgBnAFAAZQByAGYAaQBsACAAQwBpAG4AegBhACAARwBlAG4A6QByAGkAYwBvBBcEMAQzBDAE 241 | OwRMBD0EOAQ5ACAEPwRABD4ERAQwBDkEOwAgAEcAcgBhAHkAUAByAG8AZgBpAGwAIABnAOkAbgDpAHIA 242 | aQBxAHUAZQAgAGcAcgBpAHMAwQBsAHQAYQBsAOEAbgBvAHMAIABzAHoA/AByAGsAZQAgAHAAcgBvAGYA 243 | aQBskBp1KHBwlo6Ccl9pY8+P8ABHAGUAbgBlAHIAaQBzAGsAIABnAHIA5QB0AG8AbgBlAHAAcgBvAGYA 244 | aQBsx3y8GAAgAEcAcgBhAHkAINUEuFzTDMd8AE8AYgBlAGMAbgD9ACABYQBlAGQA/QAgAHAAcgBvAGYA 245 | aQBsBeQF6AXVBeQF2QXcACAARwByAGEAeQAgBdsF3AXcBdkAUAByAG8AZgBpAGwAIABnAHIAaQAgAGcA 246 | ZQBuAGUAcgBpAGMAQQBsAGwAZwBlAG0AZQBpAG4AZQBzACAARwByAGEAdQBzAHQAdQBmAGUAbgAtAFAA 247 | cgBvAGYAaQBsAFAAcgBvAGYAaQBsAG8AIABnAHIAaQBnAGkAbwAgAGcAZQBuAGUAcgBpAGMAbwBHAGUA 248 | bgBlAHIAaQBzAGsAIABnAHIA5QBzAGsAYQBsAGUAcAByAG8AZgBpAGxmbpAacHBepmPPj/Blh072TgCC 249 | LDCwMOwwpDDXMO0w1TChMKQw6wOTA7UDvQO5A7oDzAAgA8ADwQO/A8YDrwO7ACADswO6A8EDuQBQAGUA 250 | cgBmAGkAbAAgAGcAZQBuAOkAcgBpAGMAbwAgAGQAZQAgAGMAaQBuAHoAZQBuAHQAbwBzAEEAbABnAGUA 251 | bQBlAGUAbgAgAGcAcgBpAGoAcwBwAHIAbwBmAGkAZQBsAFAAZQByAGYAaQBsACAAZwByAGkAcwAgAGcA 252 | ZQBuAOkAcgBpAGMAbw5CDhsOIw5EDh8OJQ5MDioONQ5ADhcOMg4XDjEOSA4nDkQOGwBHAGUAbgBlAGwA 253 | IABHAHIAaQAgAFAAcgBvAGYAaQBsAGkAWQBsAGUAaQBuAGUAbgAgAGgAYQByAG0AYQBhAHAAcgBvAGYA 254 | aQBpAGwAaQBHAGUAbgBlAHIAaQENAGsAaQAgAHAAcgBvAGYAaQBsACAAcwBpAHYAaQBoACAAdABvAG4A 255 | bwB2AGEAVQBuAGkAdwBlAHIAcwBhAGwAbgB5ACAAcAByAG8AZgBpAGwAIABzAHoAYQByAG8BWwBjAGkE 256 | HgQxBEkEOAQ5ACAEQQQ1BEAESwQ5ACAEPwRABD4ERAQ4BDsETAZFBkQGQQAgBioGOQYxBkoGQQAgAEcA 257 | cgBhAHkAIAYnBkQGOQYnBkUARwBlAG4AZQByAGUAbAAgAGcAcgDlAHQAbwBuAGUAYgBlAHMAawByAGkA 258 | dgBlAGwAcwBlAAB0ZXh0AAAAAENvcHlyaWdodCAyMDA3IEFwcGxlIEluYy4sIGFsbCByaWdodHMgcmVz 259 | ZXJ2ZWQuAFhZWiAAAAAAAADzUQABAAAAARbMY3VydgAAAAAAAAABAc0AANIlJicoWiRjbGFzc25hbWVY 260 | JGNsYXNzZXNfEBBOU0JpdG1hcEltYWdlUmVwoycpKlpOU0ltYWdlUmVwWE5TT2JqZWN00iUmLC1XTlNB 261 | cnJheaIsKtIlJi8wXk5TTXV0YWJsZUFycmF5oy8sKtMyMwo0NTZXTlNXaGl0ZVxOU0NvbG9yU3BhY2VE 262 | MCAwABADgAzSJSY4OVdOU0NvbG9yojgq0iUmOzxXTlNJbWFnZaI7Kl8QD05TS2V5ZWRBcmNoaXZlctE/ 263 | QFRyb290gAEACAARABoAIwAtADIANwBGAEwAVwBeAGUAcgB5AIEAgwCFAIoAjACOAJUAmgClAKcAqQCr 264 | ALAAswC1ALcAuQC7AMAA1wDZANsJiwmQCZsJpAm3CbsJxgnPCdQJ3AnfCeQJ8wn3Cf4KBgoTChgKGgoc 265 | CiEKKQosCjEKOQo8Ck4KUQpWAAAAAAAAAgEAAAAAAAAAQQAAAAAAAAAAAAAAAAAAClg 266 | 267 | 268 | 269 | 270 | -------------------------------------------------------------------------------- /Sources/Prefpane Sources/main.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return., Gregorio Litenstein. 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | 10 | import PreferencePanes 11 | 12 | /** Main PreferencePane class */ 13 | 14 | class SWDAMainPrefPane: NSPreferencePane { 15 | @IBOutlet weak var mainCustomView: NSView! 16 | @IBOutlet weak var tabViewController: SWDATabViewController? 17 | 18 | /** Populate our utility singleton with instances of the views and TabView Controller; initialize tabs. */ 19 | override func assignMainView() { 20 | ControllersRef.sharedInstance.tabViewController = self.tabViewController 21 | ControllersRef.sharedInstance.thePrefPane = self 22 | ControllersRef.sharedInstance.theMainView = self.mainCustomView 23 | super.assignMainView() 24 | } 25 | /** Add the TabView Controller to the Main View and load content for the default tab. */ 26 | override func mainViewDidLoad() { 27 | super.mainViewDidLoad() 28 | self.tabViewController!.view.translatesAutoresizingMaskIntoConstraints = false 29 | ControllersRef.sharedInstance.theMainView.addSubview(self.tabViewController!.tabView) 30 | } 31 | /** Initialize the content array when the pane is first opened. */ 32 | override func didSelect() { 33 | ControllersRef.TabData.getContentArray(for: (ControllersRef.sharedInstance.tabViewController?.tabViewItems[0].view as! SWDATabTemplate), initialSetup: true) 34 | } 35 | 36 | @IBAction func showAboutDialog(_ sender: NSButton) { 37 | let mainBundle = Bundle(identifier: "cl.fail.lordkamina.SwiftDefaultApps") 38 | let appVersionString: String = mainBundle?.object(forInfoDictionaryKey:"CFBundleShortVersionString") as! String 39 | let buildNumberString: String = mainBundle?.object(forInfoDictionaryKey:"CFBundleVersion") as! String 40 | 41 | let alert = NSAlert() 42 | alert.window.title = "About" 43 | alert.messageText = "SwiftDefaultApps, v. \(appVersionString) build \(buildNumberString)" 44 | alert.informativeText = "by Gregorio Litenstein." 45 | alert.icon = ControllersRef.appIcon 46 | alert.accessoryView = HyperlinkTextField(frame: NSRect(x: 0, y:10, width:330, height:18), url: "http://www.github.com/Lord-Kamina/SwiftDefaultApps") 47 | 48 | alert.alertStyle = .informational 49 | alert.addButton(withTitle: "OK") 50 | alert.layout() 51 | 52 | DispatchQueue.main.async { 53 | alert.runModal() 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /SwiftDefaultApps CLI.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftDefaultApps CLI.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftDefaultApps CLI.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /SwiftDefaultApps CLI.xcodeproj/project.xcworkspace/xcuserdata/Koji.xcuserdatad/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildLocationStyle 6 | UseAppPreferences 7 | CustomBuildLocationType 8 | RelativeToDerivedData 9 | DerivedDataLocationStyle 10 | Default 11 | EnabledFullIndexStoreVisibility 12 | 13 | IssueFilterStyle 14 | ShowActiveSchemeOnly 15 | LiveSourceIssuesEnabled 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /SwiftDefaultApps CLI.xcodeproj/xcshareddata/xcschemes/Build CLI.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 55 | 57 | 60 | 61 | 67 | 68 | 69 | 70 | 71 | 72 | 74 | 80 | 81 | 82 | 83 | 84 | 85 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /SwiftDefaultApps CLI.xcodeproj/xcshareddata/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SchemeUserState 5 | 6 | SwDefaultApps.xcscheme 7 | 8 | 9 | SuppressBuildableAutocreation 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /SwiftDefaultApps.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftDefaultApps.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftDefaultApps.xcworkspace/xcshareddata/SwiftDefaultApps.xcscmblueprint: -------------------------------------------------------------------------------- 1 | { 2 | "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "6905E4CA861C743F3E8A8C5447173D531DEA16D7", 3 | "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : { 4 | 5 | }, 6 | "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "945ED44B-7987-41E0-BEAC-F39B17716DDC", 7 | "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : { 8 | "6905E4CA861C743F3E8A8C5447173D531DEA16D7" : "SwiftDefaultApps\/", 9 | "1FA2FB8C2BC57F03F22E05F4E40A3BD4587C910E" : "SwiftDefaultApps\/Packages\/SwiftCLI-2.0.3\/", 10 | }, 11 | "DVTSourceControlWorkspaceBlueprintNameKey" : "SwiftDefaultApps", 12 | "DVTSourceControlWorkspaceBlueprintVersion" : 204, 13 | "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "SwiftDefaultApps.xcworkspace", 14 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [ 15 | { 16 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:Lord-Kamina\/SwiftCLI.git", 17 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 18 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "1FA2FB8C2BC57F03F22E05F4E40A3BD4587C910E" 19 | }, 20 | { 21 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/Lord-Kamina\/SwiftDefaultApps.git", 22 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 23 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "6905E4CA861C743F3E8A8C5447173D531DEA16D7" 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /SwiftDefaultApps.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildSystemType 6 | Latest 7 | 8 | 9 | --------------------------------------------------------------------------------