diff --git a/BrewServicesMenubar.xcodeproj/project.pbxproj b/BrewServicesMenubar.xcodeproj/project.pbxproj index a9d5f15..a4444dd 100644 --- a/BrewServicesMenubar.xcodeproj/project.pbxproj +++ b/BrewServicesMenubar.xcodeproj/project.pbxproj @@ -85,7 +85,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0730; - LastUpgradeCheck = 0730; + LastUpgradeCheck = 0830; ORGANIZATIONNAME = andrewnicolaou; TargetAttributes = { 49ED77AD1CD4B524000B4479 = { @@ -161,8 +161,10 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "-"; @@ -206,8 +208,10 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "-"; @@ -226,6 +230,7 @@ MACOSX_DEPLOYMENT_TARGET = 10.11; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; }; name = Release; }; diff --git a/BrewServicesMenubar/AppDelegate.swift b/BrewServicesMenubar/AppDelegate.swift index e244109..df8457c 100644 --- a/BrewServicesMenubar/AppDelegate.swift +++ b/BrewServicesMenubar/AppDelegate.swift @@ -8,56 +8,42 @@ import Cocoa +let brewExecutableKey = "brewExecutable" + struct Service { var name = "" - var state = "unknown" // "started", "stopped", "unknown" + var state = "unknown" // "started", "stopped", "error", "unknown" + var user = "" } -func matchesForRegexInText(_ regex: String!, text: String!) -> [String] { - do { - let regex = try NSRegularExpression(pattern: regex, options: []) - let nsString = text as NSString - let results = regex.matches(in: text, - options: [], range: NSMakeRange(0, nsString.length)) - return results.map { nsString.substring(with: $0.range)} - } catch let error as NSError { - print("invalid regex: \(error.localizedDescription)") - return [] - } -} - - @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { - @IBOutlet weak var window: NSWindow! @IBOutlet weak var statusMenu: NSMenu! - + // Returns a status item from the system menu bar of variable length let statusItem = NSStatusBar.system().statusItem(withLength: -1) - var services = [Service]() + var services: [Service]? func applicationDidFinishLaunching(_ aNotification: Notification) { + UserDefaults.standard.register(defaults: [ + brewExecutableKey: "/usr/local/bin/brew" + ]) + let icon = NSImage(named: "icon") icon?.isTemplate = true - + if let button = statusItem.button { button.image = icon button.action = #selector(AppDelegate.handleMenuOpen(_:)) } - - queryServicesAndUpdateMenu() - } - - func applicationWillTerminate(_ aNotification: Notification) { - // Insert code here to tear down your application } // // Event handlers for UI actions // func handleClick(_ sender: NSMenuItem) { - if (sender.state == NSOnState) { + if sender.state == NSOnState { sender.state = NSOffState controlService(sender.title, state: "stop") } else { @@ -65,84 +51,150 @@ class AppDelegate: NSObject, NSApplicationDelegate { controlService(sender.title, state: "start") } } - + func handleQuit(_ sender: NSMenuItem) { - NSApplication.shared().terminate(nil) + NSApp.terminate(nil) } - + func handleMenuOpen(_ sender: AnyObject?) { queryServicesAndUpdateMenu() statusItem.popUpMenu(statusMenu) } - + // - // Update menu of services + // Update menu of services // func updateMenu() { statusMenu.removeAllItems() - for service in services { - let item = NSMenuItem.init(title: service.name, action:#selector(AppDelegate.handleClick(_:)), keyEquivalent: "") - if service.state == "started" { - item.state = NSOnState + + if let services = services { + let user = NSUserName() + for service in services { + let item = NSMenuItem.init(title: service.name, action: nil, keyEquivalent: "") + + if service.state == "started" { + item.state = NSOnState + } else if service.state == "stopped" { + item.state = NSOffState + } else { + item.state = NSMixedState + item.isEnabled = false + } + + if service.user != "" && service.user != user { + item.isEnabled = false + } + + if item.isEnabled { + item.action = #selector(AppDelegate.handleClick(_:)) + } + + statusMenu.addItem(item) } + if services.count == 0 { + let item = NSMenuItem.init(title: "No services available", action: nil, keyEquivalent: "") + item.isEnabled = false + statusMenu.addItem(item) + } + } else { + let item = NSMenuItem.init(title: "Querying services...", action: nil, keyEquivalent: "") + item.isEnabled = false statusMenu.addItem(item) } - statusMenu.addItem(NSMenuItem.separator()) - let quit = NSMenuItem.init(title: "Quit", action:#selector(AppDelegate.handleQuit(_:)), keyEquivalent: "q") - statusMenu.addItem(quit) + + statusMenu.addItem(.separator()) + statusMenu.addItem( + .init(title: "Quit", action:#selector(AppDelegate.handleQuit(_:)), keyEquivalent: "q") + ) } - + func queryServicesAndUpdateMenu() { - services = serviceStates() + services = nil updateMenu() + + DispatchQueue.global(qos: .userInitiated).async { + let result = self.serviceStates() + DispatchQueue.main.async { + self.services = result + self.updateMenu() + } + } } - + + // + // Locate homebrew + // + func brewExecutable() -> String { + return UserDefaults.standard.string(forKey: brewExecutableKey)! + } + // // Changes a service state // func controlService(_ name:String, state:String) { - let task = Process() - let outpipe = Pipe() - task.standardOutput = outpipe - - task.launchPath = "/usr/local/bin/brew" - task.arguments = ["services", state, name] - task.launch() + DispatchQueue.global(qos: .userInitiated).async { + let task = Process() + task.launchPath = self.brewExecutable() + task.arguments = ["services", state, name] + + task.launch() + task.waitUntilExit() + + if task.terminationStatus != 0 { + DispatchQueue.main.async { + let alert = NSAlert.init() + alert.alertStyle = .critical + alert.messageText = "Could not \(state) \(name)" + alert.informativeText = "You will need to manually resolve the issue." + alert.runModal() + } + } + } } - + // // Queries and parses the output of: // brew services list // func serviceStates() -> [Service] { + let launchPath = self.brewExecutable() + if !FileManager.default.isExecutableFile(atPath: launchPath) { + return [] + } + let task = Process() let outpipe = Pipe() - task.standardOutput = outpipe - - task.launchPath = "/usr/local/bin/brew" + task.launchPath = launchPath task.arguments = ["services", "list"] + task.standardOutput = outpipe + task.launch() - let outdata = outpipe.fileHandleForReading.readDataToEndOfFile() + task.waitUntilExit() + + if task.terminationStatus != 0 { + return [] + } + if var string = String(data: outdata, encoding: String.Encoding.utf8) { string = string.trimmingCharacters(in: CharacterSet.newlines) return parseServiceList(string) } - + return [] } - - let matcher = "([^ ]+)([^ ]+)" func parseServiceList(_ raw: String) -> [Service] { let rawServices = raw.components(separatedBy: "\n") return rawServices[1.. Service { - let parts = matchesForRegexInText(matcher, text: raw) - let service = Service(name: parts[0], state: parts[1]) - return service; + let parts = raw.components(separatedBy: " ").filter() { $0 != "" } + return Service( + name: parts[0], + state: parts.count >= 2 ? parts[1] : "unknown", + user: parts.count >= 3 ? parts[2] : "" + ) } } - diff --git a/BrewServicesMenubar/Base.lproj/MainMenu.xib b/BrewServicesMenubar/Base.lproj/MainMenu.xib index ad52c9d..521f74d 100644 --- a/BrewServicesMenubar/Base.lproj/MainMenu.xib +++ b/BrewServicesMenubar/Base.lproj/MainMenu.xib @@ -1,8 +1,8 @@ - - + + - + @@ -18,662 +18,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Default - - - - - - - Left to Right - - - - - - - Right to Left - - - - - - - - - - - Default - - - - - - - Left to Right - - - - - - - Right to Left - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/README.md b/README.md index d72a92a..3313364 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,17 @@ This reads the [homebrew-services](https://github.com/Homebrew/homebrew-services 1. Install [homebrew-services](https://github.com/Homebrew/homebrew-services) 2. Download from the [Releases](https://github.com/andrewn/brew-services-menubar/releases) page. +By default looks for `/usr/local/bin/brew`. If this not correct for your setup, +you can customize it using: + +```sh +defaults write andrewnicolaou.BrewServicesMenubar brewExecutable /usr/local/bin/brew +``` + ## Contributors - Andrew Nicolaou (https://github.com/andrewn) +- Stéphan Kochen (https://github.com/stephank) ## License