Skip to content

Commit

Permalink
Merge pull request #4 from stephank/master
Browse files Browse the repository at this point in the history
Various changes
  • Loading branch information
andrewn authored Apr 22, 2017
2 parents cac9ecc + 2b631c9 commit 7965a87
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 722 deletions.
7 changes: 6 additions & 1 deletion BrewServicesMenubar.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0730;
LastUpgradeCheck = 0730;
LastUpgradeCheck = 0830;
ORGANIZATIONNAME = andrewnicolaou;
TargetAttributes = {
49ED77AD1CD4B524000B4479 = {
Expand Down Expand Up @@ -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 = "-";
Expand Down Expand Up @@ -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 = "-";
Expand All @@ -226,6 +230,7 @@
MACOSX_DEPLOYMENT_TARGET = 10.11;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
};
name = Release;
};
Expand Down
174 changes: 113 additions & 61 deletions BrewServicesMenubar/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,141 +8,193 @@

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 {
sender.state = NSOnState
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..<rawServices.count].map(parseService)
}

func parseService(_ raw:String) -> 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] : ""
)
}
}

Loading

0 comments on commit 7965a87

Please sign in to comment.