├── BonjourMenu ├── Plex.icns ├── Assets.xcassets │ ├── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── main.m ├── AppDelegate.h ├── FPNetServiceBrowser.h ├── NSNetService+Additions.h ├── Info.plist ├── types.plist ├── FPNetServiceBrowser.m ├── AppDelegate.m ├── NSNetService+Additions.m └── Base.lproj │ └── Main.storyboard ├── BonjourMenu.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata ├── xcuserdata │ └── fparsons.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── project.pbxproj ├── LICENSE ├── README.md └── .gitignore /BonjourMenu/Plex.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snosrap/BonjourMenu/HEAD/BonjourMenu/Plex.icns -------------------------------------------------------------------------------- /BonjourMenu/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /BonjourMenu.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /BonjourMenu/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // BonjourMenu 4 | // 5 | // Created by Ford Parsons on 11/21/17. 6 | // Copyright © 2017 Ford Parsons. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | int main(int argc, const char * argv[]) { 12 | return NSApplicationMain(argc, argv); 13 | } 14 | -------------------------------------------------------------------------------- /BonjourMenu/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // BonjourMenu 4 | // 5 | // Created by Ford Parsons on 11/21/17. 6 | // Copyright © 2017 Ford Parsons. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "FPNetServiceBrowser.h" 11 | #import "NSNetService+Additions.h" 12 | 13 | @interface AppDelegate : NSObject 14 | @end 15 | -------------------------------------------------------------------------------- /BonjourMenu.xcodeproj/xcuserdata/fparsons.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | BonjourMenu.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /BonjourMenu/FPNetServiceBrowser.h: -------------------------------------------------------------------------------- 1 | // 2 | // FPNetServiceBrowser.h 3 | // BonjourMenu 4 | // 5 | // Created by Ford Parsons on 12/12/17. 6 | // Copyright © 2017 Ford Parsons. All rights reserved. 7 | // 8 | // http://www.dns-sd.org/ServiceTypes.html 9 | 10 | #import 11 | #import "NSNetService+Additions.h" 12 | 13 | @protocol FPNetServiceBrowserDelegate 14 | - (void)receivedServices:(NSArray *)services; 15 | @end 16 | 17 | @interface FPNetServiceBrowser : NSObject 18 | @property id delegate; 19 | @property NSMutableDictionary *deviceMap; 20 | - (void)searchForServicesOfTypes:(NSArray *)types; 21 | - (void)stop; 22 | @end 23 | 24 | @protocol FPNetServiceTypeBrowserDelegate 25 | - (void)receivedTypes:(NSArray *)types; 26 | @end 27 | 28 | @interface FPNetServiceTypeBrowser : NSObject 29 | @property id delegate; 30 | - (void)searchForTypes; 31 | - (void)stop; 32 | @end 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ford Parsons 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /BonjourMenu/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /BonjourMenu/NSNetService+Additions.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSNetService+Additions.h 3 | // BonjourMenu 4 | // 5 | // Created by Ford Parsons on 12/22/17. 6 | // Copyright © 2017 Ford Parsons. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface FPNetServiceTXTRecord : NSObject 12 | @property NSNetService *netService; 13 | - (NSString *)objectForKeyedSubscript:(NSString *)key; 14 | @end 15 | 16 | @interface NSNetService (FPNetServiceAddtions) 17 | - (NSString *)fp_ipv4; 18 | - (NSString *)fp_ipv6; 19 | - (FPNetServiceTXTRecord *)fp_TXTRecord; 20 | - (NSDictionary *)fp_dictionaryFromTXTRecordData; 21 | - (NSString *)fp_typeName; 22 | - (NSURL *)fp_URL; 23 | - (NSString *)fp_discoveredType; 24 | - (NSString *)fp_model; 25 | @end 26 | 27 | @interface NSNetService (FPMenuAddtions) 28 | - (NSMenuItem *)fp_menuItem:(SEL)action; 29 | - (NSMenu *)fp_submenuItems:(SEL)action; 30 | @end 31 | 32 | @interface NSMenuItem (FPMenuAddtions) 33 | + (NSMenuItem *)fp_itemWithTitle:(NSString *)title URL:(NSURL *)URL type:(NSString *)type action:(SEL)action; 34 | - (void)fp_imageString:(NSString *)image; 35 | @end 36 | 37 | @interface NSData (FPMenuAddtions) 38 | - (NSDictionary *)fp_parseTXTData; 39 | @end 40 | -------------------------------------------------------------------------------- /BonjourMenu/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | Copyright © 2017 Ford Parsons. All rights reserved. 27 | NSMainStoryboardFile 28 | Main 29 | NSPrincipalClass 30 | NSApplication 31 | LSUIElement 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BonjourMenu 2 | **BonjourMenu** adds a system status menu that lists local network Bonjour services. 3 | 4 | ## Background 5 | Safari previously included Bonjour webpages and printers in the bookmarks menu, however this feature was removed in Safari 11. **BonjourMenu** restores this functionality and moves the menu to the system menu bar so it is always available. 6 | 7 | ## Screenshot 8 | screen shot 9 | 10 | ## Features 11 | In addition to restoring access to Bonjour webpages and printers, **BonjourMenu** also displays other common services, facilitating access to Mac/Windows file shares, Plex media servers, Raspberry Pis (advertising with Avahi), and IoT devices. 12 | 13 | | Type | Service | Launch URL | Application | 14 | | --- | --- | --- | --- | 15 | | _http._tcp. | HTTP | `http://:

@server:port/` | Safari | 16 | | _afpovertcp._tcp. | Apple File Protocol | `afp://:

@server:port/` | Finder | 17 | | _smb._tcp. | Samba | `smb://:

@server:port/` | Finder | 18 | | _ssh._tcp. | SSH | `ssh://:

@server:port/` | Terminal | 19 | | _rfb._tcp. | VNC | `vnc://:

@server:port/` | Screen Sharing | 20 | | _plexmediasvr._tcp. | Plex | `http://:

@server:port/` | Safari | 21 | | _ipp._tcp. | Internet Printing Protocol | `ipp://:

@server:port/` | Preferences | 22 | 23 | Note: the ``, `

`, and `` variables are set via the Bonjour TXT record [as specified in the RFC](http://www.dns-sd.org/ServiceTypes.html) 24 | 25 | ## Similar 26 | [Bonjour Browser](http://tildesoft.com) by Lily Ballard 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ignore 2 | 3 | # Xcode 4 | # 5 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 6 | 7 | ## User settings 8 | xcuserdata/ 9 | 10 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 11 | *.xcscmblueprint 12 | *.xccheckout 13 | 14 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 15 | build/ 16 | DerivedData/ 17 | *.moved-aside 18 | *.pbxuser 19 | !default.pbxuser 20 | *.mode1v3 21 | !default.mode1v3 22 | *.mode2v3 23 | !default.mode2v3 24 | *.perspectivev3 25 | !default.perspectivev3 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | 30 | ## App packaging 31 | *.ipa 32 | *.dSYM.zip 33 | *.dSYM 34 | 35 | # CocoaPods 36 | # 37 | # We recommend against adding the Pods directory to your .gitignore. However 38 | # you should judge for yourself, the pros and cons are mentioned at: 39 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 40 | # 41 | # Pods/ 42 | # 43 | # Add this line if you want to avoid checking in source code from the Xcode workspace 44 | # *.xcworkspace 45 | 46 | # Carthage 47 | # 48 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 49 | # Carthage/Checkouts 50 | 51 | Carthage/Build/ 52 | 53 | # fastlane 54 | # 55 | # It is recommended to not store the screenshots in the git repo. 56 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 57 | # For more information about the recommended setup visit: 58 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 59 | 60 | fastlane/report.xml 61 | fastlane/Preview.html 62 | fastlane/screenshots/**/*.png 63 | fastlane/test_output 64 | 65 | # Code Injection 66 | # 67 | # After new code Injection tools there's a generated folder /iOSInjectionProject 68 | # https://github.com/johnno1962/injectionforxcode 69 | 70 | iOSInjectionProject/ 71 | -------------------------------------------------------------------------------- /BonjourMenu/types.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | types 6 | 7 | _http._tcp. 8 | 9 | scheme 10 | http 11 | image 12 | com.apple.Safari 13 | typeName 14 | HTTP 15 | 16 | _afpovertcp._tcp. 17 | 18 | scheme 19 | afp 20 | image 21 | /System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericAirDiskIcon.icns 22 | typeName 23 | AFP 24 | 25 | _adisk._tcp. 26 | 27 | scheme 28 | afp 29 | image 30 | /System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericAirDiskIcon.icns 31 | typeName 32 | Disk 33 | 34 | _smb._tcp. 35 | 36 | scheme 37 | smb 38 | image 39 | /System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericFileServerIcon.icns 40 | typeName 41 | Samba 42 | 43 | _ssh._tcp. 44 | 45 | scheme 46 | ssh 47 | image 48 | com.apple.Terminal 49 | typeName 50 | SSH 51 | 52 | _sftp-ssh._tcp. 53 | 54 | scheme 55 | sftp 56 | image 57 | com.apple.Terminal 58 | typeName 59 | SFTP 60 | 61 | _rfb._tcp. 62 | 63 | scheme 64 | vnc 65 | image 66 | com.apple.ScreenSharing 67 | typeName 68 | VNC 69 | 70 | _plexmediasvr._tcp. 71 | 72 | scheme 73 | http 74 | image 75 | Plex 76 | path 77 | /web 78 | typeName 79 | Plex 80 | 81 | _ipp._tcp. 82 | 83 | scheme 84 | ipp 85 | image 86 | /System/Library/PreferencePanes/PrintAndScan.prefPane/Contents/Resources/PrintScanPref.icns 87 | path 88 | /ipp/ 89 | typeName 90 | IPP 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /BonjourMenu/FPNetServiceBrowser.m: -------------------------------------------------------------------------------- 1 | // 2 | // FPNetServiceBrowser.m 3 | // BonjourMenu 4 | // 5 | // Created by Ford Parsons on 12/12/17. 6 | // Copyright © 2017 Ford Parsons. All rights reserved. 7 | // 8 | 9 | #import "FPNetServiceBrowser.h" 10 | 11 | @interface FPNetServiceBrowser () { 12 | NSMutableArray *browsers; 13 | NSMutableArray *services; 14 | NSMutableArray *devices; 15 | NSTimer *timer; 16 | } 17 | @end 18 | 19 | @implementation FPNetServiceBrowser 20 | 21 | - (void)searchForServicesOfTypes:(NSArray *)types { 22 | [self stop]; 23 | browsers = NSMutableArray.array; 24 | services = NSMutableArray.array; 25 | devices = NSMutableArray.array; 26 | self.deviceMap = NSMutableDictionary.dictionary; 27 | [types enumerateObjectsUsingBlock:^(NSString * _Nonnull type, NSUInteger idx, BOOL * _Nonnull stop) { 28 | NSNetServiceBrowser *browser = NSNetServiceBrowser.new; 29 | browser.delegate = self; 30 | [browser searchForServicesOfType:type inDomain:@""]; 31 | [browsers addObject:browser]; 32 | }]; 33 | } 34 | 35 | - (void)stop { 36 | [browsers makeObjectsPerformSelector:@selector(stop)]; 37 | [services makeObjectsPerformSelector:@selector(stop)]; 38 | browsers = nil; 39 | services = nil; 40 | } 41 | 42 | #pragma mark NSNetServiceBrowserDelegate 43 | 44 | - (void)netServiceBrowser:(NSNetServiceBrowser *)browser didFindService:(NSNetService *)service moreComing:(BOOL)moreComing { 45 | [services addObject:service]; 46 | service.delegate = self; 47 | [service resolveWithTimeout:5]; 48 | // https://stackoverflow.com/questions/4309740/how-do-i-obtain-model-name-for-a-networked-device-potentially-using-bonjour/5294662#5294662 49 | if(![[devices valueForKeyPath:@"@distinctUnionOfObjects.name"] containsObject:service.name]) { 50 | NSNetService *device = [[NSNetService alloc] initWithDomain:@"local" type:@"_device-info._tcp" name:service.name]; 51 | device.delegate = self; 52 | [device startMonitoring]; 53 | [devices addObject:device]; 54 | } 55 | } 56 | 57 | - (void)netServiceBrowser:(NSNetServiceBrowser *)browser didRemoveService:(NSNetService *)service moreComing:(BOOL)moreComing { 58 | [services removeObject:service]; 59 | if(!moreComing) { [self.delegate receivedServices:services]; } 60 | } 61 | 62 | #pragma mark NSNetServiceDelegate 63 | 64 | - (void)netServiceDidResolveAddress:(NSNetService *)sender { 65 | if(timer != nil) { return; } 66 | timer = [NSTimer timerWithTimeInterval:1 repeats:NO block:^(NSTimer * _Nonnull _timer) { 67 | [self.delegate receivedServices:self->services]; 68 | [self->timer invalidate]; 69 | self->timer = nil; 70 | }]; 71 | [NSRunLoop.mainRunLoop addTimer:timer forMode:NSRunLoopCommonModes]; 72 | } 73 | 74 | - (void)netService:(NSNetService *)sender didUpdateTXTRecordData:(NSData *)data { 75 | [self.deviceMap setObject:sender.fp_model forKey:sender.name]; 76 | [sender stopMonitoring]; 77 | } 78 | 79 | @end 80 | 81 | @interface FPNetServiceTypeBrowser () { 82 | NSNetServiceBrowser *browser; 83 | NSMutableSet *types; 84 | } 85 | @end 86 | 87 | @implementation FPNetServiceTypeBrowser 88 | - (void)searchForTypes { 89 | if(!!browser) return; 90 | [self stop]; 91 | types = NSMutableSet.set; 92 | browser = NSNetServiceBrowser.new; 93 | browser.includesPeerToPeer = YES; 94 | browser.delegate = self; 95 | [browser searchForServicesOfType:@"_services._dns-sd._udp." inDomain:@""]; 96 | } 97 | 98 | - (void)stop { 99 | [browser stop]; 100 | types = nil; 101 | browser = nil; 102 | } 103 | 104 | #pragma mark NSNetServiceBrowserDelegate 105 | 106 | - (void)netServiceBrowser:(NSNetServiceBrowser *)browser didFindService:(NSNetService *)service moreComing:(BOOL)moreComing { 107 | [types addObject:service.fp_discoveredType]; 108 | if(!moreComing) { [self.delegate receivedTypes:types.allObjects]; } 109 | } 110 | 111 | - (void)netServiceBrowser:(NSNetServiceBrowser *)browser didRemoveService:(NSNetService *)service moreComing:(BOOL)moreComing { 112 | [types removeObject:service.fp_discoveredType]; 113 | if(!moreComing) { [self.delegate receivedTypes:types.allObjects]; } 114 | } 115 | @end 116 | -------------------------------------------------------------------------------- /BonjourMenu/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // BonjourMenu 4 | // 5 | // Created by Ford Parsons on 11/21/17. 6 | // Copyright © 2017 Ford Parsons. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @interface AppDelegate () { 12 | FPNetServiceTypeBrowser *typeBrowser; 13 | FPNetServiceBrowser *browser; 14 | NSStatusItem *mainStatusItem; 15 | } 16 | @end 17 | 18 | @implementation AppDelegate 19 | 20 | - (NSDictionary *)types { 21 | return [NSUserDefaults.standardUserDefaults objectForKey:NSStringFromSelector(_cmd)]; 22 | } 23 | 24 | #pragma mark NSApplicationDelegate 25 | 26 | - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { 27 | [NSUserDefaults.standardUserDefaults registerDefaults:[NSDictionary dictionaryWithContentsOfFile:[NSBundle.mainBundle pathForResource:@"types" ofType:@"plist"]]]; 28 | 29 | mainStatusItem = [NSStatusBar.systemStatusBar statusItemWithLength:NSSquareStatusItemLength]; 30 | mainStatusItem.button.image = [NSImage imageNamed:NSImageNameBonjour]; 31 | mainStatusItem.button.image.template = YES; 32 | mainStatusItem.button.image.size = NSMakeSize(16, 16); 33 | mainStatusItem.menu = NSMenu.new; 34 | 35 | typeBrowser = FPNetServiceTypeBrowser.new; 36 | typeBrowser.delegate = self; 37 | [typeBrowser searchForTypes]; 38 | 39 | browser = FPNetServiceBrowser.new; 40 | browser.delegate = self; 41 | } 42 | 43 | #pragma mark FPNetServiceTypeBrowserDelegate 44 | 45 | - (void)receivedTypes:(NSArray *)types { 46 | [browser searchForServicesOfTypes:types]; 47 | } 48 | 49 | #pragma mark FPNetServiceBrowserDelegate 50 | 51 | - (void)receivedServices:(NSArray *)services { 52 | [mainStatusItem.menu removeAllItems]; 53 | 54 | NSArray *types = [[services valueForKeyPath:@"@distinctUnionOfObjects.type"] sortedArrayUsingSelector:@selector(compare:)]; 55 | NSArray *hostNames = [[services valueForKeyPath:@"@distinctUnionOfObjects.hostName"] sortedArrayUsingSelector:@selector(compare:)]; 56 | 57 | [types enumerateObjectsUsingBlock:^(NSString * _Nonnull type, NSUInteger idx_type, BOOL * _Nonnull stop_type) { 58 | if(![self.types.allKeys containsObject:type]) return; // continue; 59 | [[[services filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSNetService *service, NSDictionary * _Nullable bindings) { return [service.type isEqualToString:type]; }]] sortedArrayUsingComparator:^NSComparisonResult(NSNetService *obj1, NSNetService *obj2) { return [obj1.name compare:obj2.name]; }] enumerateObjectsUsingBlock:^(NSNetService * _Nonnull service, NSUInteger idx_service, BOOL * _Nonnull stop_service) { 60 | [mainStatusItem.menu addItem:[service fp_menuItem:@selector(statusItemAction:)]]; 61 | }]; 62 | [mainStatusItem.menu addItem:NSMenuItem.separatorItem]; 63 | }]; 64 | [hostNames enumerateObjectsUsingBlock:^(NSString * _Nonnull hostName, NSUInteger idx_hostName, BOOL * _Nonnull stop_hostName) { 65 | NSMenuItem *menuItem = [mainStatusItem.menu addItemWithTitle:hostName action:nil keyEquivalent:@""]; 66 | NSString *image = browser.deviceMap[[services filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSNetService *device, NSDictionary * _Nullable bindings) { 67 | return [device.hostName isEqualToString:hostName] && self->browser.deviceMap[device.name] != nil; 68 | }]].firstObject.name]; 69 | [menuItem fp_imageString:image]; 70 | menuItem.submenu = NSMenu.new; 71 | [[[services filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSNetService *service, NSDictionary * _Nullable bindings) { return [service.hostName isEqualToString:hostName]; }]] sortedArrayUsingComparator:^NSComparisonResult(NSNetService *obj1, NSNetService *obj2) { return [obj1.type compare:obj2.type] ?: [obj1.name compare:obj2.name]; }] enumerateObjectsUsingBlock:^(NSNetService * _Nonnull service, NSUInteger idx_service, BOOL * _Nonnull stop_service) { 72 | [mainStatusItem.menu.itemArray.lastObject.submenu addItem:[service fp_menuItem:@selector(statusItemAction:)]]; 73 | }]; 74 | }]; 75 | [mainStatusItem.menu addItem:NSMenuItem.separatorItem]; 76 | [types enumerateObjectsUsingBlock:^(NSString * _Nonnull type, NSUInteger idx_type, BOOL * _Nonnull stop_type) { 77 | NSMenuItem *menuItem = [mainStatusItem.menu addItemWithTitle:type action:nil keyEquivalent:@""]; 78 | menuItem.submenu = NSMenu.new; 79 | [[[services filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSNetService *service, NSDictionary * _Nullable bindings) { return [service.type isEqualToString:type]; }]] sortedArrayUsingComparator:^NSComparisonResult(NSNetService *obj1, NSNetService *obj2) { return [obj1.name compare:obj2.name]; }] enumerateObjectsUsingBlock:^(NSNetService * _Nonnull service, NSUInteger idx_service, BOOL * _Nonnull stop_service) { 80 | [mainStatusItem.menu.itemArray.lastObject.submenu addItem:[service fp_menuItem:@selector(statusItemAction:)]]; 81 | }]; 82 | }]; 83 | [mainStatusItem.menu addItem:NSMenuItem.separatorItem]; 84 | [mainStatusItem.menu addItemWithTitle:@"Quit" action:@selector(quit:) keyEquivalent:@"q"]; 85 | } 86 | 87 | #pragma mark IBAction 88 | 89 | - (IBAction)quit:(id)sender { 90 | [NSApplication.sharedApplication terminate:sender]; 91 | } 92 | 93 | - (IBAction)statusItemAction:(NSMenuItem *)sender { 94 | [NSWorkspace.sharedWorkspace openURL:sender.representedObject]; 95 | } 96 | 97 | @end 98 | -------------------------------------------------------------------------------- /BonjourMenu/NSNetService+Additions.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSNetService+Additions.m 3 | // BonjourMenu 4 | // 5 | // Created by Ford Parsons on 12/22/17. 6 | // Copyright © 2017 Ford Parsons. All rights reserved. 7 | // 8 | 9 | #import "NSNetService+Additions.h" 10 | #include 11 | #import 12 | 13 | @implementation FPNetServiceTXTRecord 14 | - (NSString *)objectForKeyedSubscript:(NSString *)key { 15 | NSData *data = [NSNetService dictionaryFromTXTRecordData:self.netService.TXTRecordData][key]; 16 | return (data != nil) ? [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] : nil; 17 | } 18 | @end 19 | 20 | @implementation NSNetService (FPNetServiceAddtions) 21 | - (NSString *)fp_ipv4 { 22 | for(NSData *data in self.addresses) { 23 | struct sockaddr *addr = (struct sockaddr *)data.bytes; 24 | if(addr->sa_family == AF_INET) { 25 | struct sockaddr_in *ip4 = (struct sockaddr_in *)data.bytes; 26 | char dest[INET_ADDRSTRLEN]; 27 | return [NSString stringWithFormat:@"%s", inet_ntop(AF_INET, &ip4->sin_addr, dest, sizeof dest)]; 28 | } 29 | } 30 | return nil; 31 | } 32 | - (NSString *)fp_ipv6 { 33 | for(NSData *data in self.addresses) { 34 | struct sockaddr *addr = (struct sockaddr *)data.bytes; 35 | if(addr->sa_family == AF_INET6) { 36 | struct sockaddr_in6 *ip6 = (struct sockaddr_in6 *)data.bytes; 37 | char dest[INET6_ADDRSTRLEN]; 38 | return [NSString stringWithFormat:@"%s", inet_ntop(AF_INET6, &ip6->sin6_addr, dest, sizeof dest)]; 39 | } 40 | } 41 | return nil; 42 | } 43 | - (FPNetServiceTXTRecord *)fp_TXTRecord { 44 | FPNetServiceTXTRecord *txtRecord = objc_getAssociatedObject(self, @selector(fp_TXTRecord)); 45 | if(!txtRecord) { 46 | txtRecord = FPNetServiceTXTRecord.new; 47 | txtRecord.netService = self; 48 | objc_setAssociatedObject(self, @selector(fp_TXTRecord), txtRecord, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 49 | } 50 | return txtRecord; 51 | } 52 | - (NSDictionary *)fp_dictionaryFromTXTRecordData { 53 | return [NSNetService dictionaryFromTXTRecordData:self.TXTRecordData]; 54 | } 55 | - (NSString *)fp_typeName { 56 | NSDictionary *dict = [NSUserDefaults.standardUserDefaults objectForKey:@"types"][self.type]; 57 | return dict[@"typeName"] ?: [[self.type componentsSeparatedByString:@"."].firstObject stringByReplacingOccurrencesOfString:@"_" withString:@""]; 58 | } 59 | - (NSURL *)fp_URL { 60 | NSDictionary *dict = [NSUserDefaults.standardUserDefaults objectForKey:@"types"][self.type]; 61 | NSURLComponents *url = NSURLComponents.new; 62 | url.scheme = dict[@"scheme"] ?: @"http"; 63 | url.user = self.fp_TXTRecord[@"u"]; 64 | url.password = self.fp_TXTRecord[@"p"]; 65 | url.host = self.hostName; 66 | if(self.port >= 0) { url.port = @(self.port); } 67 | url.path = self.fp_TXTRecord[@"path"] ?: dict[@"path"]; 68 | return url.URL; 69 | } 70 | - (NSString *)fp_discoveredType { 71 | return [NSString stringWithFormat:@"%@.%@.", self.name, [self.type componentsSeparatedByString:@"."].firstObject]; 72 | } 73 | // http://cocoadev.github.io/GettingTheIconOfNetworkMachinesTypes/ 74 | // TODO: http://www.openradar.me/30927725 75 | - (NSString *)fp_model { 76 | // Uses undocumented _LSIconPath. kUTTypeIconFileKey doesn't seem to exist any more and when it does, it lacks `/Contents/Resources/` and isn't found in CoreTypes 77 | NSString *model = self.fp_TXTRecord[@"model"]; 78 | if(!model) return nil; 79 | CFStringRef uti = CFAutorelease(UTTypeCreatePreferredIdentifierForTag((__bridge CFStringRef)@"com.apple.device-model-code", (__bridge CFStringRef)model, nil)); 80 | if(!uti) return nil; 81 | CFDictionaryRef decl = CFAutorelease(UTTypeCopyDeclaration(uti)); 82 | if(!decl) return nil; 83 | CFStringRef icon = CFDictionaryGetValue(decl, @"_LSIconPath"); 84 | while(icon == nil && uti != nil) { 85 | CFArrayRef utis = CFDictionaryGetValue(decl, kUTTypeConformsToKey); 86 | for(CFIndex i = 0; i 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | Default 530 | 531 | 532 | 533 | 534 | 535 | 536 | Left to Right 537 | 538 | 539 | 540 | 541 | 542 | 543 | Right to Left 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | Default 555 | 556 | 557 | 558 | 559 | 560 | 561 | Left to Right 562 | 563 | 564 | 565 | 566 | 567 | 568 | Right to Left 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | --------------------------------------------------------------------------------