diff --git a/README.md b/README.md index 27cdb31..4714173 100644 --- a/README.md +++ b/README.md @@ -19,37 +19,65 @@ In a file named `~/.config/shbar/shbar.json`, add a file using the following str [ { "titleRefreshInterval" : 120, - "title" : "IP Address", "mode" : "RefreshingItem", - "titleScript" : { - "bin" : "/bin/sh", + "title" : "IP Address", + "actionScript" : { + "bin" : "\/bin\/sh", "args" : [ "-c", - "echo IP: $(curl https://api.ipify.org)" + "open https:\/\/api.ipify.org" ], "env" : { - "PATH" : "/usr/bin:/usr/local/bin:/sbin:/bin" + "PATH" : "\/usr\/bin:\/usr\/local\/bin:\/sbin:\/bin" } - } - }, { - "autostartJob" : true, - "jobScript" : { - "bin" : "/bin/bash", + }, + "titleScript" : { + "bin" : "\/bin\/sh", "args" : [ "-c", - "ssh user@example.com -nNT -L 8080:localhost:8080" + "echo $(curl https:\/\/api.ipify.org) | tr '\n' ' '" ], "env" : { - "PATH" : "/usr/bin:/usr/local/bin:/sbin:/bin" + "PATH" : "\/usr\/bin:\/usr\/local\/bin:\/sbin:\/bin" } - }, - "title" : "SSH Tunnel", - "mode" : "JobStatus", - "reloadJob" : false - }, { - "mode" : "ApplicationQuit", - "title" : "Quit", - "shortcutKey" : "q" + } + }, + { + "reloadJob" : false, + "autostartJob" : false, + "title" : "~:$", + "actionShowsConsole" : false, + "mode" : "RefreshingItem", + "children" : [ + { + "autostartJob" : false, + "mode" : "RefreshingItem", + "title" : "Setup Help", + "actionScript" : { + "bin" : "\/bin\/sh", + "args" : [ + "-c", + "open https:\/\/github.com\/richinfante\/shbar" + ], + "env" : { + "PATH" : "\/usr\/bin:\/usr\/local\/bin:\/sbin:\/bin" + } + }, + "children" : [ + + ], + "actionShowsConsole" : false, + "reloadJob" : false + }, + { + "shortcutKey" : "q", + "autostartJob" : false, + "mode" : "ApplicationQuit", + "title" : "Quit", + "actionShowsConsole" : false, + "reloadJob" : false + } + ] } ] diff --git a/shbar2.xcodeproj/project.pbxproj b/shbar2.xcodeproj/project.pbxproj index c7a359d..7344b04 100644 --- a/shbar2.xcodeproj/project.pbxproj +++ b/shbar2.xcodeproj/project.pbxproj @@ -11,6 +11,9 @@ 9B45A9A2221B7B2600305441 /* ItemConfigMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B45A9A1221B7B2600305441 /* ItemConfigMode.swift */; }; 9B45A9A4221B7B4000305441 /* JobStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B45A9A3221B7B4000305441 /* JobStatus.swift */; }; 9B45A9A6221B7B6000305441 /* ItemConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B45A9A5221B7B6000305441 /* ItemConfig.swift */; }; + 9B4912D2221CCAD2006E6C73 /* LabelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B4912D1221CCAD2006E6C73 /* LabelProtocol.swift */; }; + 9B53792522296C9100D88798 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 9B53792422296C9000D88798 /* README.md */; }; + 9B53792722296C9800D88798 /* LICENSE.md in Resources */ = {isa = PBXBuildFile; fileRef = 9B53792622296C9800D88798 /* LICENSE.md */; }; 9BDEE78222163235006BA354 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BDEE78122163235006BA354 /* AppDelegate.swift */; }; 9BDEE78622163235006BA354 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9BDEE78522163235006BA354 /* Assets.xcassets */; }; 9BDEE78922163235006BA354 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9BDEE78722163235006BA354 /* Main.storyboard */; }; @@ -32,6 +35,9 @@ 9B45A9A1221B7B2600305441 /* ItemConfigMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemConfigMode.swift; sourceTree = ""; }; 9B45A9A3221B7B4000305441 /* JobStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobStatus.swift; sourceTree = ""; }; 9B45A9A5221B7B6000305441 /* ItemConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemConfig.swift; sourceTree = ""; }; + 9B4912D1221CCAD2006E6C73 /* LabelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelProtocol.swift; sourceTree = ""; }; + 9B53792422296C9000D88798 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 9B53792622296C9800D88798 /* LICENSE.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = ""; }; 9BDEE77E22163235006BA354 /* shbar2.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = shbar2.app; sourceTree = BUILT_PRODUCTS_DIR; }; 9BDEE78122163235006BA354 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 9BDEE78522163235006BA354 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -64,6 +70,8 @@ 9BDEE77522163234006BA354 = { isa = PBXGroup; children = ( + 9B53792422296C9000D88798 /* README.md */, + 9B53792622296C9800D88798 /* LICENSE.md */, 9BDEE78022163235006BA354 /* shbar2 */, 9BDEE79322163235006BA354 /* shbar2Tests */, 9BDEE77F22163235006BA354 /* Products */, @@ -91,6 +99,7 @@ 9BDEE78722163235006BA354 /* Main.storyboard */, 9BDEE78A22163235006BA354 /* Info.plist */, 9BDEE78B22163235006BA354 /* shbar2.entitlements */, + 9B4912D1221CCAD2006E6C73 /* LabelProtocol.swift */, ); path = shbar2; sourceTree = ""; @@ -193,6 +202,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9B53792722296C9800D88798 /* LICENSE.md in Resources */, + 9B53792522296C9100D88798 /* README.md in Resources */, 9BDEE78622163235006BA354 /* Assets.xcassets in Resources */, 9BDEE78922163235006BA354 /* Main.storyboard in Resources */, ); @@ -216,6 +227,7 @@ 9B45A9A4221B7B4000305441 /* JobStatus.swift in Sources */, 9B45A9A0221B7B0B00305441 /* Script.swift in Sources */, 9BDEE78222163235006BA354 /* AppDelegate.swift in Sources */, + 9B4912D2221CCAD2006E6C73 /* LabelProtocol.swift in Sources */, 9B45A9A6221B7B6000305441 /* ItemConfig.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/shbar2/AppDelegate.swift b/shbar2/AppDelegate.swift index c3708ee..94302ed 100644 --- a/shbar2/AppDelegate.swift +++ b/shbar2/AppDelegate.swift @@ -17,29 +17,39 @@ class AppDelegate: NSObject, NSApplicationDelegate { title: "IP Address", titleScript: Script( bin: "/bin/sh", - args: ["-c", "echo IP: $(curl https://api.ipify.org)"], + args: ["-c", "echo $(curl https://api.ipify.org) | tr '\n' ' '"], env: [ "PATH": "/usr/bin:/usr/local/bin:/sbin:/bin" ]), - titleRefreshInterval: 120 - ), - ItemConfig( - title: "Setup Help", + titleRefreshInterval: 120, actionScript: Script( bin: "/bin/sh", - args: ["-c", "open /~https://github.com/richinfante/shbar"], + args: ["-c", "open https://api.ipify.org"], env: [ "PATH": "/usr/bin:/usr/local/bin:/sbin:/bin" ]) ), ItemConfig( - mode: .ApplicationQuit, - title: "Quit", - shortcutKey: "q" - ) + title: "~:$", + children: [ + ItemConfig( + title: "Setup Help", + actionScript: Script( + bin: "/bin/sh", + args: ["-c", "open /~https://github.com/richinfante/shbar"], + env: [ + "PATH": "/usr/bin:/usr/local/bin:/sbin:/bin" + ]) + ), + ItemConfig( + mode: .ApplicationQuit, + title: "Quit", + shortcutKey: "q" + ) + ]) ] - let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) + var statusItems : [NSStatusItem] = [] static var userHomeDirectoryPath : String { let pw = getpwuid(getuid()) @@ -81,35 +91,59 @@ class AppDelegate: NSObject, NSApplicationDelegate { let data = try? jsonEncoder.encode(menuItems) print(String(data: data!, encoding: .utf8)!) - // Insert code here to initialize your application - let menu = NSMenu(title: "shbar") - menu.items = [] - + for item in menuItems { - // Add to actual menu - menu.items.append(item.createMenuItem(self)) + let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) + // Set main title + statusItem.menu = item.createSubMenu(self) + item.menuItem = statusItem.button! + item.initializeTitle() + + if item.actionScript != nil { + statusItem.button!.action = #selector(ItemConfig.dispatchAction) + statusItem.button!.target = item + } + +// if item.menuItem?.title == "SHBAR" { +// item.menuItem?.title = "" +// let image = NSImage(named: "Image-1") +//// image!.size = NSSize(width: NSStatusItem.squareLength, height: NSStatusItem.squareLength) +// statusItem.length = NSStatusItem.squareLength +// statusItem.button!.image = image +// } + + statusItems.append(statusItem) } - - // Set main title - statusItem.button!.title = "shbar" - statusItem.menu = menu + print("launched.") } + func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { + self.terminateRemainingJobs() + return NSApplication.TerminateReply.terminateNow + } + func applicationWillTerminate(_ aNotification: Notification) { - - // Insert code here to tear down your application + self.terminateRemainingJobs() } - @objc func terminateMenuBarApp(_ sender: NSMenuItem?) { + func terminateRemainingJobs() { + print("terminating remaining jobs...") + // Terminate jobs for item in menuItems { item.currentJob?.terminate() } + } + @objc func terminateMenuBarApp(_ sender: NSMenuItem?) { + self.terminateRemainingJobs() NSApplication.shared.terminate(self) } + func handler(sig: Int32) -> Void { + self.terminateMenuBarApp(nil) + } } diff --git a/shbar2/Assets.xcassets/AppIcon.appiconset/Contents.json b/shbar2/Assets.xcassets/AppIcon.appiconset/Contents.json index 2db2b1c..16ff491 100644 --- a/shbar2/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/shbar2/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,58 +1,71 @@ { "images" : [ { - "idiom" : "mac", "size" : "16x16", + "idiom" : "mac", + "filename" : "icon_16x16.png", "scale" : "1x" }, { - "idiom" : "mac", "size" : "16x16", + "idiom" : "mac", + "filename" : "icon_16x16@2x.png", "scale" : "2x" }, { - "idiom" : "mac", "size" : "32x32", + "idiom" : "mac", + "filename" : "icon_32x32.png", "scale" : "1x" }, { - "idiom" : "mac", "size" : "32x32", + "idiom" : "mac", + "filename" : "icon_32x32@2x.png", "scale" : "2x" }, { - "idiom" : "mac", "size" : "128x128", + "idiom" : "mac", + "filename" : "icon_128x128.png", "scale" : "1x" }, { - "idiom" : "mac", "size" : "128x128", + "idiom" : "mac", + "filename" : "icon_128x128@2x.png", "scale" : "2x" }, { - "idiom" : "mac", "size" : "256x256", + "idiom" : "mac", + "filename" : "icon_256x256.png", "scale" : "1x" }, { - "idiom" : "mac", "size" : "256x256", + "idiom" : "mac", + "filename" : "icon_256x256@2x.png", "scale" : "2x" }, { - "idiom" : "mac", "size" : "512x512", + "idiom" : "mac", + "filename" : "icon_512x512.png", "scale" : "1x" }, { - "idiom" : "mac", "size" : "512x512", + "idiom" : "mac", + "filename" : "icon_512x512@2x.png", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" + }, + "properties" : { + "pre-rendered" : true } } \ No newline at end of file diff --git a/shbar2/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/shbar2/Assets.xcassets/AppIcon.appiconset/icon_128x128.png new file mode 100644 index 0000000..aa5c35f Binary files /dev/null and b/shbar2/Assets.xcassets/AppIcon.appiconset/icon_128x128.png differ diff --git a/shbar2/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/shbar2/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png new file mode 100644 index 0000000..e1257a7 Binary files /dev/null and b/shbar2/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png differ diff --git a/shbar2/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/shbar2/Assets.xcassets/AppIcon.appiconset/icon_16x16.png new file mode 100644 index 0000000..f752173 Binary files /dev/null and b/shbar2/Assets.xcassets/AppIcon.appiconset/icon_16x16.png differ diff --git a/shbar2/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/shbar2/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png new file mode 100644 index 0000000..d430f74 Binary files /dev/null and b/shbar2/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png differ diff --git a/shbar2/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/shbar2/Assets.xcassets/AppIcon.appiconset/icon_256x256.png new file mode 100644 index 0000000..e1257a7 Binary files /dev/null and b/shbar2/Assets.xcassets/AppIcon.appiconset/icon_256x256.png differ diff --git a/shbar2/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/shbar2/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png new file mode 100644 index 0000000..5aef3de Binary files /dev/null and b/shbar2/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png differ diff --git a/shbar2/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/shbar2/Assets.xcassets/AppIcon.appiconset/icon_32x32.png new file mode 100644 index 0000000..d430f74 Binary files /dev/null and b/shbar2/Assets.xcassets/AppIcon.appiconset/icon_32x32.png differ diff --git a/shbar2/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/shbar2/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png new file mode 100644 index 0000000..4f30c13 Binary files /dev/null and b/shbar2/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png differ diff --git a/shbar2/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/shbar2/Assets.xcassets/AppIcon.appiconset/icon_512x512.png new file mode 100644 index 0000000..5aef3de Binary files /dev/null and b/shbar2/Assets.xcassets/AppIcon.appiconset/icon_512x512.png differ diff --git a/shbar2/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/shbar2/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png new file mode 100644 index 0000000..d20d8c4 Binary files /dev/null and b/shbar2/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png differ diff --git a/shbar2/Info.plist b/shbar2/Info.plist index b407442..ef27ffa 100644 --- a/shbar2/Info.plist +++ b/shbar2/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.0.1 + 0.0.2 CFBundleVersion - 8 + 10 LSApplicationCategoryType public.app-category.developer-tools LSMinimumSystemVersion diff --git a/shbar2/ItemConfig.swift b/shbar2/ItemConfig.swift index b56c6cf..69c2b0d 100644 --- a/shbar2/ItemConfig.swift +++ b/shbar2/ItemConfig.swift @@ -19,7 +19,7 @@ class ItemConfig : Codable { var jobScript: Script? var reloadJob: Bool? var autostartJob: Bool? - var menuItem: NSMenuItem? + var menuItem: LabelProtocol? var refreshTimer: Timer? var jobStatusItem: NSMenuItem? var jobExitStatus: Int32? @@ -58,7 +58,8 @@ class ItemConfig : Codable { jobScript: Script? = nil, reloadJob: Bool? = false, autostartJob: Bool? = false, - shortcutKey: String? = nil + shortcutKey: String? = nil, + children: [ItemConfig]? = [] ) { self.mode = mode self.title = title @@ -69,6 +70,7 @@ class ItemConfig : Codable { self.jobScript = jobScript self.reloadJob = reloadJob self.autostartJob = autostartJob + self.children = children } @objc func suspendJob() { @@ -207,15 +209,20 @@ class ItemConfig : Codable { // Assign title mutableAttributedString.append(NSAttributedString(string: " " + title)) - menuItem?.attributedTitle = mutableAttributedString + menuItem?.attributedTitleString = mutableAttributedString } else { - menuItem?.title = title + if menuItem?.allowsNewlines == true { + menuItem?.title = title + } else { + menuItem?.title = title.replacingOccurrences(of: "\n", with: " ") + } } } - func createMenuItem(_ appDelegate: AppDelegate) -> NSMenuItem { - let menuItem = NSMenuItem() - + + /// Generate the submenu for this item + // If it has none, nil is returned. + func createSubMenu(_ appDelegate: AppDelegate) -> NSMenu? { if let children = self.children, children.count > 0 { let subMenu = NSMenu() subMenu.autoenablesItems = true @@ -223,13 +230,14 @@ class ItemConfig : Codable { for item in children { subMenu.addItem(item.createMenuItem(appDelegate)) } - menuItem.submenu = subMenu + + return subMenu } - + if self.mode == .JobStatus { let subMenu = NSMenu() subMenu.autoenablesItems = false - + let statusItem = NSMenuItem(title: "stopped", action: nil, keyEquivalent: "") statusItem.isEnabled = false self.jobStatusItem = statusItem @@ -258,7 +266,7 @@ class ItemConfig : Codable { resumeItem.action = #selector(ItemConfig.resumeJob) resumeItem.isEnabled = false resumeItem.target = self - + let consoleItem = NSMenuItem(title: "View Console", action: nil, keyEquivalent: "") consoleItem.action = #selector(ItemConfig.showJobConsole) consoleItem.isEnabled = true @@ -281,13 +289,20 @@ class ItemConfig : Codable { self.resumeMenuItem = resumeItem self.consoleMenuItem = consoleItem + return subMenu + } + + return nil + } + + /// Create a menu item for this. + func createMenuItem(_ appDelegate: AppDelegate) -> NSMenuItem { + let menuItem = NSMenuItem() + + let subMenu = self.createSubMenu(appDelegate) + + if let subMenu = subMenu { menuItem.submenu = subMenu - - - // Start the job if needed - if let autoStart = self.autostartJob, autoStart { - self.startJob() - } } else if self.mode == .ApplicationQuit { menuItem.target = appDelegate menuItem.action = #selector(AppDelegate.terminateMenuBarApp(_:)) @@ -299,6 +314,11 @@ class ItemConfig : Codable { } } + // Start the job if needed + if let autoStart = self.autostartJob, autoStart { + self.startJob() + } + // Shortcut key if let shortcutKey = self.shortcutKey { menuItem.keyEquivalent = shortcutKey @@ -351,6 +371,7 @@ class ItemConfig : Codable { titleScript.execute { [weak self] _, result in // Update in background. self?.menuItem?.title = result + print(result) // Invalidate if self is no longer available (gc) if self == nil { timer.invalidate() diff --git a/shbar2/LabelProtocol.swift b/shbar2/LabelProtocol.swift new file mode 100644 index 0000000..ea1c145 --- /dev/null +++ b/shbar2/LabelProtocol.swift @@ -0,0 +1,44 @@ +// +// LabelProtocol.swift +// shbar2 +// +// Created by Rich Infante on 2/19/19. +// Copyright © 2019 Rich Infante. All rights reserved. +// + +import Cocoa +import Foundation + +/// Provides a generic wrapper around various menu and label types +protocol LabelProtocol { + var title : String { get set } + var attributedTitleString: NSAttributedString? { get set } + var allowsNewlines : Bool { get } +} + +extension NSMenuItem : LabelProtocol { + var allowsNewlines : Bool { return false } + var attributedTitleString: NSAttributedString? { + get { + return self.attributedTitle + } + set { + self.attributedTitle = newValue + } + } +} + +extension NSStatusBarButton : LabelProtocol { + var allowsNewlines : Bool { return false } + var attributedTitleString: NSAttributedString? { + get { + return self.attributedTitle + } + set { + if let newValue = newValue { + self.attributedTitle = newValue + } + } + } +} +