@import Cocoa; @interface AppDelegate : NSObject @end static NSMutableDictionary *nameCache; // ################################################################ // # // # MARK: - AppId - // # // ################################################################ @interface AppId : NSObject @property (copy) NSString *bundleId; @property (copy) NSString *name; @end @implementation AppId + (instancetype)bundleId:(NSString*)bundleId { AppId *a = [AppId new]; a.bundleId = bundleId; [a updateAppName]; return a; } /// First query name cache for available names. If not set, add new name to cache - (void)updateAppName { self.name = nameCache[self.bundleId]; if (!self.name) { self.name = [self applicationNameForBundleId:self.bundleId]; if (!self.name) self.name = self.bundleId; [nameCache setValue:self.name forKey:self.bundleId]; } } /// Returns application name for given identifier - (NSString*)applicationNameForBundleId:(NSString*)bundleID { NSArray *urls = CFBridgingRelease(LSCopyApplicationURLsForBundleIdentifier((__bridge CFStringRef)bundleID, NULL)); if (urls.count > 0) { NSDictionary *info = CFBridgingRelease(CFBundleCopyInfoDictionaryForURL((CFURLRef)urls.firstObject)); return info[(NSString*)kCFBundleExecutableKey]; } return nil; } @end // ################################################################ // # // # MARK: - Scheme - // # // ################################################################ @interface Scheme : NSObject @property (copy) NSString *name; @property (weak) AppId *registered; @property (strong) NSArray *available; @end @implementation Scheme + (instancetype)name:(NSString*)name { Scheme *s = [Scheme new]; s.name = name; [s prepareAvailable]; return s; } /// Select app at index and set it default. Checks whether set successful. Ignores setting same id. - (void)setDefault:(NSUInteger)index { AppId *app = self.available[index]; if (app == self.registered) return; OSStatus s = LSSetDefaultHandlerForURLScheme((__bridge CFStringRef)self.name, (__bridge CFStringRef)app.bundleId); if (s == 0) self.registered = app; } /// Gathers all registered application for scheme and inserts to available - (void)prepareAvailable { NSMutableArray *list = [NSMutableArray array]; NSString *defaultId = CFBridgingRelease(LSCopyDefaultHandlerForURLScheme((__bridge CFStringRef)self.name)); NSArray *ids = CFBridgingRelease(LSCopyAllHandlersForURLScheme((__bridge CFStringRef)self.name)); // LSCopyDefaultRoleHandlerForContentType, LSCopyAllRoleHandlersForContentType, kLSRolesAll for (NSString *bundleId in ids) { [list addObject:[AppId bundleId:bundleId]]; if ([bundleId isEqualToString:defaultId]) self.registered = list.lastObject; } self.available = [list sortedArrayUsingComparator:^NSComparisonResult(AppId *a, AppId *b) { return [[a.name lowercaseString] compare:[b.name lowercaseString]]; }]; } @end // ################################################################ // # // # MARK: - Main - // # // ################################################################ @interface AppDelegate () @property (weak) IBOutlet NSWindow *window; @property (weak) IBOutlet NSTableView *table; @property (strong) NSMutableArray *data; @end @implementation AppDelegate - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender { return YES; } - (void)applicationWillFinishLaunching:(NSNotification *)notification { nameCache = [NSMutableDictionary dictionary]; self.data = [NSMutableArray array]; } - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { for (NSString *urlScheme in [self readLaunchServicesSchemes]) { Scheme *s = [Scheme name:urlScheme]; if (s.available.count > 1) [self.data addObject:s]; } [self.data sortUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES]]]; [self.table reloadData]; } - (NSSet*)readLaunchServicesSchemes { NSUserDefaults *ud = [[NSUserDefaults alloc] initWithSuiteName:@"com.apple.LaunchServices/com.apple.launchservices.secure"]; NSMutableSet *allSchemes = [NSMutableSet set]; for (NSDictionary *handler in [ud arrayForKey:@"LSHandlers"]) { NSString *scheme = handler[@"LSHandlerURLScheme"]; // LSHandlerContentType if (scheme) [allSchemes addObject:scheme]; } return allSchemes; } #pragma mark - TableView & ComboBox data source // table view data source - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView { return self.data.count; } // table view data source - (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { if ([tableColumn.identifier isEqualToString:@"colScheme"]) return self.data[row].name; return self.data[row].registered.name; } // table view data source - (void)tableView:(NSTableView *)tableView sortDescriptorsDidChange:(NSArray *)oldDescriptors { [self.data sortUsingDescriptors:tableView.sortDescriptors]; [tableView setNeedsDisplay]; } // table view data source - (void)tableView:(NSTableView *)tableView setObjectValue:(nullable id)object forTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row { NSInteger idx = [[tableView selectedCell] indexOfSelectedItem]; [self.data[row] setDefault:idx]; } // combo box data source - (NSInteger)numberOfItemsInComboBoxCell:(NSComboBoxCell *)comboBoxCell { Scheme *s = self.data[self.table.selectedRow]; comboBoxCell.representedObject = s.available; return s.available.count; } // combo box data source - (id)comboBoxCell:(NSComboBoxCell *)comboBoxCell objectValueForItemAtIndex:(NSInteger)index { NSArray *apps = comboBoxCell.representedObject; return apps[index].name; } @end // Rebuild Launch Services cache // https://eclecticlight.co/2017/08/11/launch-services-database-problems-correcting-and-rebuilding/ // /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -kill -r -v -apps u