├── LICENSE.md ├── README.md ├── screenshots └── demo.png ├── shbar2.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── rich.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── shbar2 ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x.png │ └── Contents.json ├── Base.lproj │ └── Main.storyboard ├── Info.plist ├── ItemConfig.swift ├── ItemConfigMode.swift ├── JobStatus.swift ├── LabelProtocol.swift ├── Script.swift └── shbar2.entitlements └── shbar2Tests ├── Info.plist └── shbar2Tests.swift /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2019 Richard Infante 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shbar 2 | Shell Scripting + Jobs in your macOS Menu Bar! 3 | 4 | _warning: this is alpha quality software. Use at your own risk._ 5 | 6 | ![example screenshot](screenshots/demo.png) 7 | 8 | ## Known Issues 9 | - Killing the shbar app does not kill child procesess, on restart new ones are created. 10 | 11 | ## Install 12 | 1. Grab the latest release [here](https://github.com/richinfante/shbar/releases) 13 | 2. Download and place unzipped `.app` file into `/Applications` 14 | 15 | ## Setup 16 | In a file named `~/.config/shbar/shbar.json`, add a file using the following structure: 17 | 18 | ```json 19 | [ 20 | { 21 | "titleRefreshInterval" : 120, 22 | "mode" : "RefreshingItem", 23 | "title" : "IP Address", 24 | "actionScript" : { 25 | "bin" : "\/bin\/sh", 26 | "args" : [ 27 | "-c", 28 | "open https:\/\/api.ipify.org" 29 | ], 30 | "env" : { 31 | "PATH" : "\/usr\/bin:\/usr\/local\/bin:\/sbin:\/bin" 32 | } 33 | }, 34 | "titleScript" : { 35 | "bin" : "\/bin\/sh", 36 | "args" : [ 37 | "-c", 38 | "echo $(curl https:\/\/api.ipify.org) | tr '\n' ' '" 39 | ], 40 | "env" : { 41 | "PATH" : "\/usr\/bin:\/usr\/local\/bin:\/sbin:\/bin" 42 | } 43 | } 44 | }, 45 | { 46 | "reloadJob" : false, 47 | "autostartJob" : false, 48 | "title" : "~:$", 49 | "actionShowsConsole" : false, 50 | "mode" : "RefreshingItem", 51 | "children" : [ 52 | { 53 | "autostartJob" : false, 54 | "mode" : "RefreshingItem", 55 | "title" : "Setup Help", 56 | "actionScript" : { 57 | "bin" : "\/bin\/sh", 58 | "args" : [ 59 | "-c", 60 | "open https:\/\/github.com\/richinfante\/shbar" 61 | ], 62 | "env" : { 63 | "PATH" : "\/usr\/bin:\/usr\/local\/bin:\/sbin:\/bin" 64 | } 65 | }, 66 | "children" : [ 67 | 68 | ], 69 | "actionShowsConsole" : false, 70 | "reloadJob" : false 71 | }, 72 | { 73 | "shortcutKey" : "q", 74 | "autostartJob" : false, 75 | "mode" : "ApplicationQuit", 76 | "title" : "Quit", 77 | "actionShowsConsole" : false, 78 | "reloadJob" : false 79 | } 80 | ] 81 | } 82 | ] 83 | 84 | ``` 85 | 86 | 87 | ## Logging 88 | Shbar places logfiles for each process here: `~/Library/Logs/shbar/`. It does not automatically remove the logfiles, but will in a future release. 89 | -------------------------------------------------------------------------------- /screenshots/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richinfante/shbar/e9a700114570a2319b594955b36b094232225eff/screenshots/demo.png -------------------------------------------------------------------------------- /shbar2.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 9B45A9A0221B7B0B00305441 /* Script.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B45A99F221B7B0B00305441 /* Script.swift */; }; 11 | 9B45A9A2221B7B2600305441 /* ItemConfigMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B45A9A1221B7B2600305441 /* ItemConfigMode.swift */; }; 12 | 9B45A9A4221B7B4000305441 /* JobStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B45A9A3221B7B4000305441 /* JobStatus.swift */; }; 13 | 9B45A9A6221B7B6000305441 /* ItemConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B45A9A5221B7B6000305441 /* ItemConfig.swift */; }; 14 | 9B4912D2221CCAD2006E6C73 /* LabelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B4912D1221CCAD2006E6C73 /* LabelProtocol.swift */; }; 15 | 9B53792522296C9100D88798 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 9B53792422296C9000D88798 /* README.md */; }; 16 | 9B53792722296C9800D88798 /* LICENSE.md in Resources */ = {isa = PBXBuildFile; fileRef = 9B53792622296C9800D88798 /* LICENSE.md */; }; 17 | 9BDEE78222163235006BA354 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BDEE78122163235006BA354 /* AppDelegate.swift */; }; 18 | 9BDEE78622163235006BA354 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9BDEE78522163235006BA354 /* Assets.xcassets */; }; 19 | 9BDEE78922163235006BA354 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9BDEE78722163235006BA354 /* Main.storyboard */; }; 20 | 9BDEE79522163235006BA354 /* shbar2Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BDEE79422163235006BA354 /* shbar2Tests.swift */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXContainerItemProxy section */ 24 | 9BDEE79122163235006BA354 /* PBXContainerItemProxy */ = { 25 | isa = PBXContainerItemProxy; 26 | containerPortal = 9BDEE77622163234006BA354 /* Project object */; 27 | proxyType = 1; 28 | remoteGlobalIDString = 9BDEE77D22163235006BA354; 29 | remoteInfo = shbar2; 30 | }; 31 | /* End PBXContainerItemProxy section */ 32 | 33 | /* Begin PBXFileReference section */ 34 | 9B45A99F221B7B0B00305441 /* Script.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Script.swift; sourceTree = ""; }; 35 | 9B45A9A1221B7B2600305441 /* ItemConfigMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemConfigMode.swift; sourceTree = ""; }; 36 | 9B45A9A3221B7B4000305441 /* JobStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobStatus.swift; sourceTree = ""; }; 37 | 9B45A9A5221B7B6000305441 /* ItemConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemConfig.swift; sourceTree = ""; }; 38 | 9B4912D1221CCAD2006E6C73 /* LabelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelProtocol.swift; sourceTree = ""; }; 39 | 9B53792422296C9000D88798 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 40 | 9B53792622296C9800D88798 /* LICENSE.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = ""; }; 41 | 9BDEE77E22163235006BA354 /* shbar2.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = shbar2.app; sourceTree = BUILT_PRODUCTS_DIR; }; 42 | 9BDEE78122163235006BA354 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 43 | 9BDEE78522163235006BA354 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 44 | 9BDEE78822163235006BA354 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 45 | 9BDEE78A22163235006BA354 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 46 | 9BDEE78B22163235006BA354 /* shbar2.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = shbar2.entitlements; sourceTree = ""; }; 47 | 9BDEE79022163235006BA354 /* shbar2Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = shbar2Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 48 | 9BDEE79422163235006BA354 /* shbar2Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = shbar2Tests.swift; sourceTree = ""; }; 49 | 9BDEE79622163235006BA354 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50 | /* End PBXFileReference section */ 51 | 52 | /* Begin PBXFrameworksBuildPhase section */ 53 | 9BDEE77B22163235006BA354 /* Frameworks */ = { 54 | isa = PBXFrameworksBuildPhase; 55 | buildActionMask = 2147483647; 56 | files = ( 57 | ); 58 | runOnlyForDeploymentPostprocessing = 0; 59 | }; 60 | 9BDEE78D22163235006BA354 /* Frameworks */ = { 61 | isa = PBXFrameworksBuildPhase; 62 | buildActionMask = 2147483647; 63 | files = ( 64 | ); 65 | runOnlyForDeploymentPostprocessing = 0; 66 | }; 67 | /* End PBXFrameworksBuildPhase section */ 68 | 69 | /* Begin PBXGroup section */ 70 | 9BDEE77522163234006BA354 = { 71 | isa = PBXGroup; 72 | children = ( 73 | 9B53792422296C9000D88798 /* README.md */, 74 | 9B53792622296C9800D88798 /* LICENSE.md */, 75 | 9BDEE78022163235006BA354 /* shbar2 */, 76 | 9BDEE79322163235006BA354 /* shbar2Tests */, 77 | 9BDEE77F22163235006BA354 /* Products */, 78 | ); 79 | sourceTree = ""; 80 | }; 81 | 9BDEE77F22163235006BA354 /* Products */ = { 82 | isa = PBXGroup; 83 | children = ( 84 | 9BDEE77E22163235006BA354 /* shbar2.app */, 85 | 9BDEE79022163235006BA354 /* shbar2Tests.xctest */, 86 | ); 87 | name = Products; 88 | sourceTree = ""; 89 | }; 90 | 9BDEE78022163235006BA354 /* shbar2 */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | 9BDEE78122163235006BA354 /* AppDelegate.swift */, 94 | 9B45A9A5221B7B6000305441 /* ItemConfig.swift */, 95 | 9B45A9A3221B7B4000305441 /* JobStatus.swift */, 96 | 9B45A9A1221B7B2600305441 /* ItemConfigMode.swift */, 97 | 9B45A99F221B7B0B00305441 /* Script.swift */, 98 | 9B4912D1221CCAD2006E6C73 /* LabelProtocol.swift */, 99 | 9BDEE78522163235006BA354 /* Assets.xcassets */, 100 | 9BDEE78722163235006BA354 /* Main.storyboard */, 101 | 9BDEE78A22163235006BA354 /* Info.plist */, 102 | 9BDEE78B22163235006BA354 /* shbar2.entitlements */, 103 | ); 104 | path = shbar2; 105 | sourceTree = ""; 106 | }; 107 | 9BDEE79322163235006BA354 /* shbar2Tests */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | 9BDEE79422163235006BA354 /* shbar2Tests.swift */, 111 | 9BDEE79622163235006BA354 /* Info.plist */, 112 | ); 113 | path = shbar2Tests; 114 | sourceTree = ""; 115 | }; 116 | /* End PBXGroup section */ 117 | 118 | /* Begin PBXNativeTarget section */ 119 | 9BDEE77D22163235006BA354 /* shbar2 */ = { 120 | isa = PBXNativeTarget; 121 | buildConfigurationList = 9BDEE79922163235006BA354 /* Build configuration list for PBXNativeTarget "shbar2" */; 122 | buildPhases = ( 123 | 9BDEE77A22163235006BA354 /* Sources */, 124 | 9BDEE77B22163235006BA354 /* Frameworks */, 125 | 9BDEE77C22163235006BA354 /* Resources */, 126 | ); 127 | buildRules = ( 128 | ); 129 | dependencies = ( 130 | ); 131 | name = shbar2; 132 | productName = shbar2; 133 | productReference = 9BDEE77E22163235006BA354 /* shbar2.app */; 134 | productType = "com.apple.product-type.application"; 135 | }; 136 | 9BDEE78F22163235006BA354 /* shbar2Tests */ = { 137 | isa = PBXNativeTarget; 138 | buildConfigurationList = 9BDEE79C22163235006BA354 /* Build configuration list for PBXNativeTarget "shbar2Tests" */; 139 | buildPhases = ( 140 | 9BDEE78C22163235006BA354 /* Sources */, 141 | 9BDEE78D22163235006BA354 /* Frameworks */, 142 | 9BDEE78E22163235006BA354 /* Resources */, 143 | ); 144 | buildRules = ( 145 | ); 146 | dependencies = ( 147 | 9BDEE79222163235006BA354 /* PBXTargetDependency */, 148 | ); 149 | name = shbar2Tests; 150 | productName = shbar2Tests; 151 | productReference = 9BDEE79022163235006BA354 /* shbar2Tests.xctest */; 152 | productType = "com.apple.product-type.bundle.unit-test"; 153 | }; 154 | /* End PBXNativeTarget section */ 155 | 156 | /* Begin PBXProject section */ 157 | 9BDEE77622163234006BA354 /* Project object */ = { 158 | isa = PBXProject; 159 | attributes = { 160 | LastSwiftUpdateCheck = 1010; 161 | LastUpgradeCheck = 1010; 162 | ORGANIZATIONNAME = "Rich Infante"; 163 | TargetAttributes = { 164 | 9BDEE77D22163235006BA354 = { 165 | CreatedOnToolsVersion = 10.1; 166 | SystemCapabilities = { 167 | com.apple.HardenedRuntime = { 168 | enabled = 1; 169 | }; 170 | com.apple.Sandbox = { 171 | enabled = 0; 172 | }; 173 | }; 174 | }; 175 | 9BDEE78F22163235006BA354 = { 176 | CreatedOnToolsVersion = 10.1; 177 | TestTargetID = 9BDEE77D22163235006BA354; 178 | }; 179 | }; 180 | }; 181 | buildConfigurationList = 9BDEE77922163234006BA354 /* Build configuration list for PBXProject "shbar2" */; 182 | compatibilityVersion = "Xcode 9.3"; 183 | developmentRegion = en; 184 | hasScannedForEncodings = 0; 185 | knownRegions = ( 186 | en, 187 | Base, 188 | ); 189 | mainGroup = 9BDEE77522163234006BA354; 190 | productRefGroup = 9BDEE77F22163235006BA354 /* Products */; 191 | projectDirPath = ""; 192 | projectRoot = ""; 193 | targets = ( 194 | 9BDEE77D22163235006BA354 /* shbar2 */, 195 | 9BDEE78F22163235006BA354 /* shbar2Tests */, 196 | ); 197 | }; 198 | /* End PBXProject section */ 199 | 200 | /* Begin PBXResourcesBuildPhase section */ 201 | 9BDEE77C22163235006BA354 /* Resources */ = { 202 | isa = PBXResourcesBuildPhase; 203 | buildActionMask = 2147483647; 204 | files = ( 205 | 9B53792722296C9800D88798 /* LICENSE.md in Resources */, 206 | 9B53792522296C9100D88798 /* README.md in Resources */, 207 | 9BDEE78622163235006BA354 /* Assets.xcassets in Resources */, 208 | 9BDEE78922163235006BA354 /* Main.storyboard in Resources */, 209 | ); 210 | runOnlyForDeploymentPostprocessing = 0; 211 | }; 212 | 9BDEE78E22163235006BA354 /* Resources */ = { 213 | isa = PBXResourcesBuildPhase; 214 | buildActionMask = 2147483647; 215 | files = ( 216 | ); 217 | runOnlyForDeploymentPostprocessing = 0; 218 | }; 219 | /* End PBXResourcesBuildPhase section */ 220 | 221 | /* Begin PBXSourcesBuildPhase section */ 222 | 9BDEE77A22163235006BA354 /* Sources */ = { 223 | isa = PBXSourcesBuildPhase; 224 | buildActionMask = 2147483647; 225 | files = ( 226 | 9B45A9A2221B7B2600305441 /* ItemConfigMode.swift in Sources */, 227 | 9B45A9A4221B7B4000305441 /* JobStatus.swift in Sources */, 228 | 9B45A9A0221B7B0B00305441 /* Script.swift in Sources */, 229 | 9BDEE78222163235006BA354 /* AppDelegate.swift in Sources */, 230 | 9B4912D2221CCAD2006E6C73 /* LabelProtocol.swift in Sources */, 231 | 9B45A9A6221B7B6000305441 /* ItemConfig.swift in Sources */, 232 | ); 233 | runOnlyForDeploymentPostprocessing = 0; 234 | }; 235 | 9BDEE78C22163235006BA354 /* Sources */ = { 236 | isa = PBXSourcesBuildPhase; 237 | buildActionMask = 2147483647; 238 | files = ( 239 | 9BDEE79522163235006BA354 /* shbar2Tests.swift in Sources */, 240 | ); 241 | runOnlyForDeploymentPostprocessing = 0; 242 | }; 243 | /* End PBXSourcesBuildPhase section */ 244 | 245 | /* Begin PBXTargetDependency section */ 246 | 9BDEE79222163235006BA354 /* PBXTargetDependency */ = { 247 | isa = PBXTargetDependency; 248 | target = 9BDEE77D22163235006BA354 /* shbar2 */; 249 | targetProxy = 9BDEE79122163235006BA354 /* PBXContainerItemProxy */; 250 | }; 251 | /* End PBXTargetDependency section */ 252 | 253 | /* Begin PBXVariantGroup section */ 254 | 9BDEE78722163235006BA354 /* Main.storyboard */ = { 255 | isa = PBXVariantGroup; 256 | children = ( 257 | 9BDEE78822163235006BA354 /* Base */, 258 | ); 259 | name = Main.storyboard; 260 | sourceTree = ""; 261 | }; 262 | /* End PBXVariantGroup section */ 263 | 264 | /* Begin XCBuildConfiguration section */ 265 | 9BDEE79722163235006BA354 /* Debug */ = { 266 | isa = XCBuildConfiguration; 267 | buildSettings = { 268 | ALWAYS_SEARCH_USER_PATHS = NO; 269 | CLANG_ANALYZER_NONNULL = YES; 270 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 271 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 272 | CLANG_CXX_LIBRARY = "libc++"; 273 | CLANG_ENABLE_MODULES = YES; 274 | CLANG_ENABLE_OBJC_ARC = YES; 275 | CLANG_ENABLE_OBJC_WEAK = YES; 276 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 277 | CLANG_WARN_BOOL_CONVERSION = YES; 278 | CLANG_WARN_COMMA = YES; 279 | CLANG_WARN_CONSTANT_CONVERSION = YES; 280 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 281 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 282 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 283 | CLANG_WARN_EMPTY_BODY = YES; 284 | CLANG_WARN_ENUM_CONVERSION = YES; 285 | CLANG_WARN_INFINITE_RECURSION = YES; 286 | CLANG_WARN_INT_CONVERSION = YES; 287 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 288 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 289 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 290 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 291 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 292 | CLANG_WARN_STRICT_PROTOTYPES = YES; 293 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 294 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 295 | CLANG_WARN_UNREACHABLE_CODE = YES; 296 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 297 | CODE_SIGN_IDENTITY = "Mac Developer"; 298 | COPY_PHASE_STRIP = NO; 299 | DEBUG_INFORMATION_FORMAT = dwarf; 300 | ENABLE_STRICT_OBJC_MSGSEND = YES; 301 | ENABLE_TESTABILITY = YES; 302 | GCC_C_LANGUAGE_STANDARD = gnu11; 303 | GCC_DYNAMIC_NO_PIC = NO; 304 | GCC_NO_COMMON_BLOCKS = YES; 305 | GCC_OPTIMIZATION_LEVEL = 0; 306 | GCC_PREPROCESSOR_DEFINITIONS = ( 307 | "DEBUG=1", 308 | "$(inherited)", 309 | ); 310 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 311 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 312 | GCC_WARN_UNDECLARED_SELECTOR = YES; 313 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 314 | GCC_WARN_UNUSED_FUNCTION = YES; 315 | GCC_WARN_UNUSED_VARIABLE = YES; 316 | MACOSX_DEPLOYMENT_TARGET = 10.14; 317 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 318 | MTL_FAST_MATH = YES; 319 | ONLY_ACTIVE_ARCH = YES; 320 | SDKROOT = macosx; 321 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 322 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 323 | }; 324 | name = Debug; 325 | }; 326 | 9BDEE79822163235006BA354 /* Release */ = { 327 | isa = XCBuildConfiguration; 328 | buildSettings = { 329 | ALWAYS_SEARCH_USER_PATHS = NO; 330 | CLANG_ANALYZER_NONNULL = YES; 331 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 332 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 333 | CLANG_CXX_LIBRARY = "libc++"; 334 | CLANG_ENABLE_MODULES = YES; 335 | CLANG_ENABLE_OBJC_ARC = YES; 336 | CLANG_ENABLE_OBJC_WEAK = YES; 337 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 338 | CLANG_WARN_BOOL_CONVERSION = YES; 339 | CLANG_WARN_COMMA = YES; 340 | CLANG_WARN_CONSTANT_CONVERSION = YES; 341 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 342 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 343 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 344 | CLANG_WARN_EMPTY_BODY = YES; 345 | CLANG_WARN_ENUM_CONVERSION = YES; 346 | CLANG_WARN_INFINITE_RECURSION = YES; 347 | CLANG_WARN_INT_CONVERSION = YES; 348 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 349 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 350 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 351 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 352 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 353 | CLANG_WARN_STRICT_PROTOTYPES = YES; 354 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 355 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 356 | CLANG_WARN_UNREACHABLE_CODE = YES; 357 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 358 | CODE_SIGN_IDENTITY = "Mac Developer"; 359 | COPY_PHASE_STRIP = NO; 360 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 361 | ENABLE_NS_ASSERTIONS = NO; 362 | ENABLE_STRICT_OBJC_MSGSEND = YES; 363 | GCC_C_LANGUAGE_STANDARD = gnu11; 364 | GCC_NO_COMMON_BLOCKS = YES; 365 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 366 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 367 | GCC_WARN_UNDECLARED_SELECTOR = YES; 368 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 369 | GCC_WARN_UNUSED_FUNCTION = YES; 370 | GCC_WARN_UNUSED_VARIABLE = YES; 371 | MACOSX_DEPLOYMENT_TARGET = 10.14; 372 | MTL_ENABLE_DEBUG_INFO = NO; 373 | MTL_FAST_MATH = YES; 374 | SDKROOT = macosx; 375 | SWIFT_COMPILATION_MODE = wholemodule; 376 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 377 | }; 378 | name = Release; 379 | }; 380 | 9BDEE79A22163235006BA354 /* Debug */ = { 381 | isa = XCBuildConfiguration; 382 | buildSettings = { 383 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 384 | CODE_SIGN_IDENTITY = "Mac Developer"; 385 | CODE_SIGN_STYLE = Automatic; 386 | COMBINE_HIDPI_IMAGES = YES; 387 | DEVELOPMENT_TEAM = 7A264T7MNQ; 388 | ENABLE_HARDENED_RUNTIME = YES; 389 | INFOPLIST_FILE = shbar2/Info.plist; 390 | LD_RUNPATH_SEARCH_PATHS = ( 391 | "$(inherited)", 392 | "@executable_path/../Frameworks", 393 | ); 394 | PRODUCT_BUNDLE_IDENTIFIER = com.richinfante.shbar2; 395 | PRODUCT_NAME = "$(TARGET_NAME)"; 396 | PROVISIONING_PROFILE_SPECIFIER = ""; 397 | SWIFT_VERSION = 4.2; 398 | }; 399 | name = Debug; 400 | }; 401 | 9BDEE79B22163235006BA354 /* Release */ = { 402 | isa = XCBuildConfiguration; 403 | buildSettings = { 404 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 405 | CODE_SIGN_IDENTITY = "Mac Developer"; 406 | CODE_SIGN_STYLE = Automatic; 407 | COMBINE_HIDPI_IMAGES = YES; 408 | DEVELOPMENT_TEAM = 7A264T7MNQ; 409 | ENABLE_HARDENED_RUNTIME = YES; 410 | INFOPLIST_FILE = shbar2/Info.plist; 411 | LD_RUNPATH_SEARCH_PATHS = ( 412 | "$(inherited)", 413 | "@executable_path/../Frameworks", 414 | ); 415 | PRODUCT_BUNDLE_IDENTIFIER = com.richinfante.shbar2; 416 | PRODUCT_NAME = "$(TARGET_NAME)"; 417 | PROVISIONING_PROFILE_SPECIFIER = ""; 418 | SWIFT_VERSION = 4.2; 419 | }; 420 | name = Release; 421 | }; 422 | 9BDEE79D22163235006BA354 /* Debug */ = { 423 | isa = XCBuildConfiguration; 424 | buildSettings = { 425 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 426 | BUNDLE_LOADER = "$(TEST_HOST)"; 427 | CODE_SIGN_STYLE = Automatic; 428 | COMBINE_HIDPI_IMAGES = YES; 429 | DEVELOPMENT_TEAM = 7A264T7MNQ; 430 | INFOPLIST_FILE = shbar2Tests/Info.plist; 431 | LD_RUNPATH_SEARCH_PATHS = ( 432 | "$(inherited)", 433 | "@executable_path/../Frameworks", 434 | "@loader_path/../Frameworks", 435 | ); 436 | PRODUCT_BUNDLE_IDENTIFIER = com.richinfante.shbar2Tests; 437 | PRODUCT_NAME = "$(TARGET_NAME)"; 438 | SWIFT_VERSION = 4.2; 439 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/shbar2.app/Contents/MacOS/shbar2"; 440 | }; 441 | name = Debug; 442 | }; 443 | 9BDEE79E22163235006BA354 /* Release */ = { 444 | isa = XCBuildConfiguration; 445 | buildSettings = { 446 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 447 | BUNDLE_LOADER = "$(TEST_HOST)"; 448 | CODE_SIGN_STYLE = Automatic; 449 | COMBINE_HIDPI_IMAGES = YES; 450 | DEVELOPMENT_TEAM = 7A264T7MNQ; 451 | INFOPLIST_FILE = shbar2Tests/Info.plist; 452 | LD_RUNPATH_SEARCH_PATHS = ( 453 | "$(inherited)", 454 | "@executable_path/../Frameworks", 455 | "@loader_path/../Frameworks", 456 | ); 457 | PRODUCT_BUNDLE_IDENTIFIER = com.richinfante.shbar2Tests; 458 | PRODUCT_NAME = "$(TARGET_NAME)"; 459 | SWIFT_VERSION = 4.2; 460 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/shbar2.app/Contents/MacOS/shbar2"; 461 | }; 462 | name = Release; 463 | }; 464 | /* End XCBuildConfiguration section */ 465 | 466 | /* Begin XCConfigurationList section */ 467 | 9BDEE77922163234006BA354 /* Build configuration list for PBXProject "shbar2" */ = { 468 | isa = XCConfigurationList; 469 | buildConfigurations = ( 470 | 9BDEE79722163235006BA354 /* Debug */, 471 | 9BDEE79822163235006BA354 /* Release */, 472 | ); 473 | defaultConfigurationIsVisible = 0; 474 | defaultConfigurationName = Release; 475 | }; 476 | 9BDEE79922163235006BA354 /* Build configuration list for PBXNativeTarget "shbar2" */ = { 477 | isa = XCConfigurationList; 478 | buildConfigurations = ( 479 | 9BDEE79A22163235006BA354 /* Debug */, 480 | 9BDEE79B22163235006BA354 /* Release */, 481 | ); 482 | defaultConfigurationIsVisible = 0; 483 | defaultConfigurationName = Release; 484 | }; 485 | 9BDEE79C22163235006BA354 /* Build configuration list for PBXNativeTarget "shbar2Tests" */ = { 486 | isa = XCConfigurationList; 487 | buildConfigurations = ( 488 | 9BDEE79D22163235006BA354 /* Debug */, 489 | 9BDEE79E22163235006BA354 /* Release */, 490 | ); 491 | defaultConfigurationIsVisible = 0; 492 | defaultConfigurationName = Release; 493 | }; 494 | /* End XCConfigurationList section */ 495 | }; 496 | rootObject = 9BDEE77622163234006BA354 /* Project object */; 497 | } 498 | -------------------------------------------------------------------------------- /shbar2.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /shbar2.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /shbar2.xcodeproj/xcuserdata/rich.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /shbar2.xcodeproj/xcuserdata/rich.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | shbar2.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /shbar2/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // shbar2 4 | // 5 | // Created by Rich Infante on 2/14/19. 6 | // Copyright © 2019 Rich Infante. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Foundation 11 | import UserNotifications 12 | 13 | @NSApplicationMain 14 | class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate { 15 | 16 | /// Store ids / itemconfig mappings. 17 | /// Used to translate jobs across notifications. 18 | var responderItems : [String:ItemConfig] = [:] 19 | 20 | 21 | /// Register a job with the responder dict. 22 | /// This allows for finding later with the returned ID. 23 | func registerProcessNotificationID(job: ItemConfig) -> String { 24 | let str = UUID.init().uuidString 25 | self.responderItems[str] = job 26 | return str 27 | } 28 | 29 | 30 | /// Get a process via it's ID 31 | func getProcessByNotificationID(id: String) -> ItemConfig? { 32 | return responderItems[id] 33 | } 34 | 35 | // Menu items to display. Set to default config with help. 36 | var menuItems : [ItemConfig] = [ 37 | ItemConfig( 38 | title: "IP Address", 39 | titleScript: Script( 40 | bin: "/bin/sh", 41 | args: ["-c", "echo $(curl https://api.ipify.org) | tr '\n' ' '"], 42 | env: [ 43 | "PATH": "/usr/bin:/usr/local/bin:/sbin:/bin" 44 | ]), 45 | titleRefreshInterval: 120, 46 | actionScript: Script( 47 | bin: "/bin/sh", 48 | args: ["-c", "open https://api.ipify.org"], 49 | env: [ 50 | "PATH": "/usr/bin:/usr/local/bin:/sbin:/bin" 51 | ]) 52 | ), 53 | ItemConfig( 54 | title: "~:$", 55 | children: [ 56 | ItemConfig( 57 | title: "Setup Help", 58 | actionScript: Script( 59 | bin: "/bin/sh", 60 | args: ["-c", "open https://github.com/richinfante/shbar"], 61 | env: [ 62 | "PATH": "/usr/bin:/usr/local/bin:/sbin:/bin" 63 | ]) 64 | ), 65 | ItemConfig( 66 | title: "Show Config Folder", 67 | actionScript: Script( 68 | bin: "/bin/sh", 69 | args: ["-c", "open \(AppDelegate.userHomeDirectoryPath)/.config/shbar/"], 70 | env: [ 71 | "PATH": "/usr/bin:/usr/local/bin:/sbin:/bin" 72 | ]) 73 | ), 74 | ItemConfig( 75 | mode: .ApplicationQuit, 76 | title: "Quit", 77 | shortcutKey: "q" 78 | ) 79 | ]) 80 | ] 81 | 82 | var statusItems : [NSStatusItem] = [] 83 | 84 | /// Get a link to the user's home path 85 | static var userHomeDirectoryPath : String { 86 | let pw = getpwuid(getuid()) 87 | let home = pw?.pointee.pw_dir 88 | let homePath = FileManager.default.string(withFileSystemRepresentation: home!, length: Int(strlen(home))) 89 | 90 | return homePath 91 | } 92 | 93 | /// Handler for app launch. 94 | func applicationDidFinishLaunching(_ aNotification: Notification) { 95 | let manager = FileManager.default 96 | 97 | let restartAction = UNNotificationAction(identifier: "restart", title: "Restart", options: []) 98 | let logsAction = UNNotificationAction(identifier: "logs", title: "View Logs", options: []) 99 | 100 | let jobAlert = UNNotificationCategory(identifier: "jobAlert", actions: [restartAction, logsAction], intentIdentifiers: [], options: []) 101 | UNUserNotificationCenter.current().setNotificationCategories([jobAlert]) 102 | 103 | 104 | // Create config directory 105 | do { 106 | try manager.createDirectory(atPath: "\(AppDelegate.userHomeDirectoryPath)/.config/shbar", withIntermediateDirectories: true) 107 | } catch let error { 108 | print("Error creating config directory: \(error)") 109 | } 110 | 111 | // Create log directory 112 | do { 113 | try manager.createDirectory(atPath: "\(AppDelegate.userHomeDirectoryPath)/Library/Logs/shbar", withIntermediateDirectories: false) 114 | } catch let error { 115 | print("Error creating log directory: \(error)") 116 | } 117 | 118 | // Attempt to decode the JSON config file. 119 | let json = try? Data(contentsOf: URL(fileURLWithPath: "\(AppDelegate.userHomeDirectoryPath)/.config/shbar/shbar.json")) 120 | 121 | // If load works, try to decode into an itemconfig. 122 | if let json = json { 123 | let decoder = JSONDecoder() 124 | let decodedItems = try? decoder.decode([ItemConfig].self, from: json) 125 | 126 | // Assign the new items into the global item list. 127 | if let decodedItems = decodedItems { 128 | print("Loaded from File!") 129 | menuItems = decodedItems 130 | } 131 | } else { 132 | print("No config file present!") 133 | } 134 | 135 | // Next, pretty-print and format the current config. 136 | let jsonEncoder = JSONEncoder() 137 | jsonEncoder.outputFormatting = .prettyPrinted 138 | 139 | // Print the config out. 140 | let data = try? jsonEncoder.encode(menuItems) 141 | print(String(data: data!, encoding: .utf8)!) 142 | 143 | 144 | // Initialize the menu items. 145 | for item in menuItems { 146 | let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) 147 | // Set main title 148 | statusItem.menu = item.createSubMenu(self) 149 | item.menuItem = statusItem.button! 150 | item.initializeTitle() 151 | 152 | // Set up action to dispatch a script. 153 | if item.actionScript != nil { 154 | statusItem.button!.action = #selector(ItemConfig.dispatchAction) 155 | statusItem.button!.target = item 156 | } 157 | 158 | // TODO: why is this incorrectly sized? 159 | // if item.menuItem?.title == "SHBAR" { 160 | // item.menuItem?.title = "" 161 | // let image = NSImage(named: "Image-1") 162 | //// image!.size = NSSize(width: NSStatusItem.squareLength, height: NSStatusItem.squareLength) 163 | // statusItem.length = NSStatusItem.squareLength 164 | // statusItem.button!.image = image 165 | // } 166 | 167 | statusItems.append(statusItem) 168 | } 169 | 170 | print("launched.") 171 | } 172 | 173 | func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { 174 | self.terminateRemainingJobs() 175 | return NSApplication.TerminateReply.terminateNow 176 | } 177 | 178 | func applicationWillTerminate(_ aNotification: Notification) { 179 | self.terminateRemainingJobs() 180 | } 181 | 182 | func terminateRemainingJobs() { 183 | print("terminating remaining jobs...") 184 | 185 | // Terminate jobs 186 | for item in menuItems { 187 | item.currentJob?.interrupt() 188 | item.currentJob?.terminate() 189 | } 190 | 191 | print("termination complete.") 192 | } 193 | 194 | @objc func terminateMenuBarApp(_ sender: NSMenuItem?) { 195 | self.terminateRemainingJobs() 196 | NSApplication.shared.terminate(self) 197 | } 198 | 199 | /// Allow in-app notifications (for when menu is focused) 200 | func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { 201 | 202 | completionHandler([.alert, .sound, .badge]) 203 | } 204 | 205 | /// Enable message handling. 206 | func userNotificationCenter(_ center: UNUserNotificationCenter, 207 | didReceive response: UNNotificationResponse, 208 | withCompletionHandler completionHandler: 209 | @escaping () -> Void) { 210 | // Get the meeting ID from the original notification. 211 | let userInfo = response.notification.request.content.userInfo 212 | 213 | // Try to get Job notification ID 214 | if let id = userInfo["job"] as? String { 215 | 216 | // Try to find associated job. 217 | if let job = self.getProcessByNotificationID(id: id) { 218 | 219 | // Parse actions 220 | if response.notification.request.content.categoryIdentifier == "jobAlert" { 221 | if response.actionIdentifier == "logs" { 222 | job.showJobConsole() 223 | } 224 | 225 | if response.actionIdentifier == "restart" || response.actionIdentifier == "start"{ 226 | job.startJob() 227 | } 228 | } 229 | } else { 230 | print("Can't find Job.") 231 | } 232 | } else { 233 | print("No job ID!") 234 | } 235 | 236 | completionHandler() 237 | } 238 | } 239 | 240 | -------------------------------------------------------------------------------- /shbar2/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "icon_16x16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "icon_16x16@2x.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "icon_32x32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "icon_32x32@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "icon_128x128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "icon_128x128@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "icon_256x256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "icon_256x256@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "icon_512x512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "icon_512x512@2x.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | }, 68 | "properties" : { 69 | "pre-rendered" : true 70 | } 71 | } -------------------------------------------------------------------------------- /shbar2/Assets.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richinfante/shbar/e9a700114570a2319b594955b36b094232225eff/shbar2/Assets.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /shbar2/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richinfante/shbar/e9a700114570a2319b594955b36b094232225eff/shbar2/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /shbar2/Assets.xcassets/AppIcon.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richinfante/shbar/e9a700114570a2319b594955b36b094232225eff/shbar2/Assets.xcassets/AppIcon.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /shbar2/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richinfante/shbar/e9a700114570a2319b594955b36b094232225eff/shbar2/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /shbar2/Assets.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richinfante/shbar/e9a700114570a2319b594955b36b094232225eff/shbar2/Assets.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /shbar2/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richinfante/shbar/e9a700114570a2319b594955b36b094232225eff/shbar2/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /shbar2/Assets.xcassets/AppIcon.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richinfante/shbar/e9a700114570a2319b594955b36b094232225eff/shbar2/Assets.xcassets/AppIcon.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /shbar2/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richinfante/shbar/e9a700114570a2319b594955b36b094232225eff/shbar2/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /shbar2/Assets.xcassets/AppIcon.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richinfante/shbar/e9a700114570a2319b594955b36b094232225eff/shbar2/Assets.xcassets/AppIcon.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /shbar2/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richinfante/shbar/e9a700114570a2319b594955b36b094232225eff/shbar2/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /shbar2/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /shbar2/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 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 | Default 529 | 530 | 531 | 532 | 533 | 534 | 535 | Left to Right 536 | 537 | 538 | 539 | 540 | 541 | 542 | Right to Left 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | Default 554 | 555 | 556 | 557 | 558 | 559 | 560 | Left to Right 561 | 562 | 563 | 564 | 565 | 566 | 567 | Right to Left 568 | 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 | -------------------------------------------------------------------------------- /shbar2/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 | 0.0.2 21 | CFBundleVersion 22 | 12 23 | LSApplicationCategoryType 24 | public.app-category.developer-tools 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | LSUIElement 28 | 29 | NSHumanReadableCopyright 30 | Copyright © 2019 Rich Infante. All rights reserved. 31 | NSMainStoryboardFile 32 | Main 33 | NSPrincipalClass 34 | NSApplication 35 | NSUserNotificationAlertStyle 36 | alert 37 | 38 | 39 | -------------------------------------------------------------------------------- /shbar2/ItemConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemConfig.swift 3 | // shbar2 4 | // 5 | // Created by Rich Infante on 2/18/19. 6 | // Copyright © 2019 Rich Infante. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Foundation 11 | import UserNotifications 12 | 13 | class ItemConfig : Codable { 14 | /// The mode for the menu item. 15 | var mode: ItemConfigMode? = .RefreshingItem 16 | 17 | /// Display title 18 | var title: String? 19 | 20 | /// Title script to run. 21 | var titleScript: Script? 22 | 23 | /// Refresh interval for the title. 24 | var titleRefreshInterval: TimeInterval? 25 | 26 | /// Script to un on click. 27 | var actionScript: Script? 28 | 29 | /// Shortcut key for the menu item 30 | var shortcutKey: String? 31 | 32 | /// For Job-Type Items, the job script. 33 | var jobScript: Script? 34 | 35 | /// Should we auto-reload the job when it fails? 36 | var reloadJob: Bool? 37 | 38 | /// Should we auto-start the job on startup. 39 | var autostartJob: Bool? 40 | 41 | /// Attachment to the menu label for updating text. 42 | /// This is a commmon protocol adapter for different types of system labels. 43 | var menuItem: LabelProtocol? 44 | 45 | /// Timer scheduling refresh events. 46 | var refreshTimer: Timer? 47 | 48 | /// Job status menu item - for jobs with status, this is a label to show "running"/"suspended"/etc.. states. 49 | var jobStatusItem: NSMenuItem? 50 | 51 | /// The exit status from the job. 52 | var jobExitStatus: Int32? 53 | 54 | /// The currently runnin job process 55 | var currentJob: Process? 56 | 57 | /// Is the job paused? 58 | var isPaused : Bool = false 59 | 60 | /// Is the action a console show? 61 | var actionShowsConsole: Bool? = false 62 | 63 | /// Children of this item. 64 | var children: [ItemConfig]? = [] 65 | 66 | /// Menu item for the start job button 67 | var startMenuItem: NSMenuItem? 68 | 69 | /// Menu item for the stop job button 70 | var stopMenuItem: NSMenuItem? 71 | 72 | /// Menu item for the restart job button 73 | var restartMenuItem: NSMenuItem? 74 | 75 | /// Menu item for the suspend job button 76 | var suspendMenuItem: NSMenuItem? 77 | 78 | /// Menu item for the resume job button 79 | var resumeMenuItem: NSMenuItem? 80 | 81 | /// Menu item for the showing console of job. 82 | var consoleMenuItem: NSMenuItem? 83 | 84 | var delegate: AppDelegate? 85 | 86 | /// Keys to encode / decode for direct-to-config (de)serialization 87 | private enum CodingKeys: String, CodingKey { 88 | case mode 89 | case children 90 | case title 91 | case titleScript 92 | case titleRefreshInterval 93 | case actionScript 94 | case shortcutKey 95 | case jobScript 96 | case reloadJob 97 | case autostartJob 98 | case actionShowsConsole 99 | } 100 | 101 | /// Initializer for building in-code. 102 | /// Primarily for demo ui. 103 | init( 104 | mode: ItemConfigMode? = .RefreshingItem, 105 | title: String? = nil, 106 | titleScript: Script? = nil, 107 | titleRefreshInterval: TimeInterval? = nil, 108 | actionScript: Script? = nil, 109 | jobScript: Script? = nil, 110 | reloadJob: Bool? = false, 111 | autostartJob: Bool? = false, 112 | shortcutKey: String? = nil, 113 | children: [ItemConfig]? = [] 114 | ) { 115 | self.mode = mode 116 | self.title = title 117 | self.titleScript = titleScript 118 | self.titleRefreshInterval = titleRefreshInterval 119 | self.actionScript = actionScript 120 | self.shortcutKey = shortcutKey 121 | self.jobScript = jobScript 122 | self.reloadJob = reloadJob 123 | self.autostartJob = autostartJob 124 | self.children = children 125 | } 126 | 127 | /// Function for suspending the current job, if it's running. 128 | /// This updates the status labels. 129 | @objc func suspendJob() { 130 | if let process = self.currentJob { 131 | process.suspend() 132 | self.isPaused = true 133 | 134 | if let title = self.title { 135 | self.updateTitle(title: title) 136 | } 137 | } 138 | } 139 | 140 | /// Function for resuming the current job, if it's stopped. 141 | /// This updates the status labels. 142 | @objc func resumeJob() { 143 | if let process = self.currentJob { 144 | process.resume() 145 | self.isPaused = false 146 | 147 | if let title = self.title { 148 | self.updateTitle(title: title) 149 | } 150 | } 151 | } 152 | 153 | /// Function for starting the current job, if it's not running. 154 | /// Running jobs are first killed. 155 | /// This updates the status labels. 156 | @objc func startJob() { 157 | self.currentJob?.interrupt() 158 | self.currentJob?.terminate() 159 | 160 | if let script = self.jobScript { 161 | script.launchJob (launched: { 162 | process in 163 | 164 | self.currentJob = process 165 | 166 | if let title = self.title { 167 | self.updateTitle(title: title) 168 | } 169 | }, completed: { 170 | status in 171 | 172 | // Register for notifications if we have a delegate. 173 | if let delegate = self.delegate { 174 | let id = delegate.registerProcessNotificationID(job: self) 175 | 176 | // Create content 177 | let content = UNMutableNotificationContent() 178 | content.title = "\(self.title ?? "Process")" 179 | content.categoryIdentifier = "jobAlert" 180 | 181 | // Show info for auto-restarts. 182 | if self.reloadJob == true { 183 | content.body = "Auto-Restarted; Exited with code: \(status)" 184 | } else { 185 | content.body = "Exited with code: \(status)" 186 | } 187 | 188 | content.sound = UNNotificationSound.default 189 | content.userInfo = [ "job": id ] 190 | 191 | // Trigger after 0.1s. 192 | let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false) 193 | 194 | // Schedule request. 195 | let request = UNNotificationRequest(identifier: "localNotification", content: content, trigger: trigger) 196 | 197 | // Set delegate and add. 198 | UNUserNotificationCenter.current().delegate = self.delegate 199 | UNUserNotificationCenter.current().add(request) { (error) in 200 | print(error) 201 | } 202 | } 203 | 204 | // update title 205 | if let title = self.title { 206 | self.updateTitle(title: title) 207 | } 208 | 209 | // Reload if needed. 210 | if let reloadJob = self.reloadJob, reloadJob { 211 | self.startJob() 212 | } 213 | }) 214 | } 215 | } 216 | 217 | /// Stop a job. 218 | @objc func stopJob() { 219 | self.reloadJob = false 220 | if let job = self.currentJob { 221 | job.terminate() 222 | self.currentJob = nil 223 | 224 | if let title = self.title { 225 | self.updateTitle(title: title) 226 | } 227 | 228 | print("Terminate current job: \(job.processIdentifier)") 229 | } else { 230 | print("Cannot terminate: no job running.") 231 | } 232 | } 233 | 234 | /// Show console. 235 | @objc func showJobConsole() { 236 | if let jobScript = self.jobScript, let uuid = jobScript.uuid { 237 | DispatchQueue.global(qos: .background).async { 238 | let process = Process() 239 | process.arguments = ["\(AppDelegate.userHomeDirectoryPath)/Library/Logs/shbar/\(uuid.uuidString).log"] 240 | process.launchPath = "/usr/bin/open" 241 | process.launch() 242 | } 243 | } else { 244 | print("Failed to show console") 245 | } 246 | } 247 | 248 | /// Update title / job status. 249 | func updateTitle(title: String) { 250 | if self.mode == .JobStatus { 251 | var color = NSColor.gray 252 | 253 | // Update job's status label 254 | if let currentJob = self.currentJob { 255 | // Determine color depending on status 256 | if self.isPaused { 257 | color = NSColor.blue 258 | } else if currentJob.isRunning { 259 | color = NSColor.green 260 | } else if currentJob.terminationStatus != 0 { 261 | color = NSColor.red 262 | } 263 | 264 | // Update job control menu items 265 | if self.isPaused { 266 | self.startMenuItem?.isEnabled = false 267 | self.stopMenuItem?.isEnabled = true 268 | self.restartMenuItem?.isEnabled = false 269 | self.suspendMenuItem?.isEnabled = false 270 | self.resumeMenuItem?.isEnabled = true 271 | self.consoleMenuItem?.isEnabled = true 272 | self.jobStatusItem?.title = "suspended - pid:\(currentJob.processIdentifier)" 273 | } else if currentJob.isRunning { 274 | self.startMenuItem?.isEnabled = false 275 | self.stopMenuItem?.isEnabled = true 276 | self.restartMenuItem?.isEnabled = true 277 | self.suspendMenuItem?.isEnabled = true 278 | self.resumeMenuItem?.isEnabled = false 279 | self.consoleMenuItem?.isEnabled = true 280 | self.jobStatusItem?.title = "running - pid:\(currentJob.processIdentifier)" 281 | } else { 282 | self.startMenuItem?.isEnabled = true 283 | self.stopMenuItem?.isEnabled = false 284 | self.restartMenuItem?.isEnabled = false 285 | self.suspendMenuItem?.isEnabled = false 286 | self.resumeMenuItem?.isEnabled = false 287 | self.consoleMenuItem?.isEnabled = true 288 | self.jobStatusItem?.title = "exited: \(currentJob.terminationStatus)" 289 | } 290 | } else { 291 | self.jobStatusItem?.title = "inactive" 292 | self.consoleMenuItem?.isEnabled = false 293 | self.startMenuItem?.isEnabled = true 294 | self.stopMenuItem?.isEnabled = false 295 | self.restartMenuItem?.isEnabled = false 296 | self.suspendMenuItem?.isEnabled = false 297 | self.resumeMenuItem?.isEnabled = false 298 | self.consoleMenuItem?.isEnabled = false 299 | } 300 | 301 | // Create status bullet 302 | let mutableAttributedString = NSMutableAttributedString(string: "●", attributes: [ 303 | NSAttributedString.Key.foregroundColor: color 304 | ]) 305 | 306 | // Assign title 307 | mutableAttributedString.append(NSAttributedString(string: " " + title)) 308 | menuItem?.attributedTitleString = mutableAttributedString 309 | } else { 310 | if menuItem?.allowsNewlines == true { 311 | menuItem?.title = title 312 | } else { 313 | menuItem?.title = title.replacingOccurrences(of: "\n", with: " ") 314 | } 315 | } 316 | } 317 | 318 | 319 | /// Generate the submenu for this item 320 | // If it has none, nil is returned. 321 | func createSubMenu(_ appDelegate: AppDelegate) -> NSMenu? { 322 | self.delegate = appDelegate 323 | if let children = self.children, children.count > 0 { 324 | let subMenu = NSMenu() 325 | subMenu.autoenablesItems = true 326 | 327 | for item in children { 328 | subMenu.addItem(item.createMenuItem(appDelegate)) 329 | } 330 | 331 | return subMenu 332 | } 333 | 334 | if self.mode == .JobStatus { 335 | let subMenu = NSMenu() 336 | subMenu.autoenablesItems = false 337 | 338 | let statusItem = NSMenuItem(title: "stopped", action: nil, keyEquivalent: "") 339 | statusItem.isEnabled = false 340 | self.jobStatusItem = statusItem 341 | 342 | let startItem = NSMenuItem(title: "Start Job", action: nil, keyEquivalent: "") 343 | startItem.action = #selector(ItemConfig.startJob) 344 | startItem.isEnabled = true 345 | startItem.target = self 346 | 347 | let stopItem = NSMenuItem(title: "Stop Job", action: nil, keyEquivalent: "") 348 | stopItem.action = #selector(ItemConfig.stopJob) 349 | stopItem.isEnabled = false 350 | stopItem.target = self 351 | 352 | let restartItem = NSMenuItem(title: "Restart Job", action: nil, keyEquivalent: "") 353 | restartItem.action = #selector(ItemConfig.startJob) 354 | restartItem.isEnabled = false 355 | restartItem.target = self 356 | 357 | let suspendItem = NSMenuItem(title: "Suspend Job", action: nil, keyEquivalent: "") 358 | suspendItem.action = #selector(ItemConfig.suspendJob) 359 | suspendItem.isEnabled = false 360 | suspendItem.target = self 361 | 362 | let resumeItem = NSMenuItem(title: "Resume Job", action: nil, keyEquivalent: "") 363 | resumeItem.action = #selector(ItemConfig.resumeJob) 364 | resumeItem.isEnabled = false 365 | resumeItem.target = self 366 | 367 | let consoleItem = NSMenuItem(title: "View Console", action: nil, keyEquivalent: "") 368 | consoleItem.action = #selector(ItemConfig.showJobConsole) 369 | consoleItem.isEnabled = true 370 | consoleItem.target = self 371 | 372 | subMenu.addItem(statusItem) 373 | subMenu.addItem(NSMenuItem.separator()) 374 | subMenu.addItem(consoleItem) 375 | subMenu.addItem(NSMenuItem.separator()) 376 | subMenu.addItem(startItem) 377 | subMenu.addItem(stopItem) 378 | subMenu.addItem(restartItem) 379 | subMenu.addItem(suspendItem) 380 | subMenu.addItem(resumeItem) 381 | 382 | self.startMenuItem = startItem 383 | self.stopMenuItem = stopItem 384 | self.restartMenuItem = restartItem 385 | self.suspendMenuItem = suspendItem 386 | self.resumeMenuItem = resumeItem 387 | self.consoleMenuItem = consoleItem 388 | 389 | return subMenu 390 | } 391 | 392 | return nil 393 | } 394 | 395 | /// Create a menu item for this. 396 | func createMenuItem(_ appDelegate: AppDelegate) -> NSMenuItem { 397 | let menuItem = NSMenuItem() 398 | self.delegate = appDelegate 399 | 400 | let subMenu = self.createSubMenu(appDelegate) 401 | 402 | if let subMenu = subMenu { 403 | menuItem.submenu = subMenu 404 | } else if self.mode == .ApplicationQuit { 405 | menuItem.target = appDelegate 406 | menuItem.action = #selector(AppDelegate.terminateMenuBarApp(_:)) 407 | } else if self.mode == .RefreshingItem { 408 | // Set up scripting 409 | if self.actionScript != nil { 410 | menuItem.target = self 411 | menuItem.action = #selector(ItemConfig.dispatchAction) 412 | } 413 | } 414 | 415 | // Start the job if needed 416 | if let autoStart = self.autostartJob, autoStart { 417 | self.startJob() 418 | } 419 | 420 | // Shortcut key 421 | if let shortcutKey = self.shortcutKey { 422 | menuItem.keyEquivalent = shortcutKey 423 | menuItem.keyEquivalentModifierMask = .command 424 | } 425 | 426 | // Associate the menu item to the model 427 | self.menuItem = menuItem 428 | self.initializeTitle() 429 | return menuItem 430 | } 431 | 432 | /// Dispatch background script action 433 | @objc func dispatchAction() { 434 | if let script = self.actionScript { 435 | script.launchJob(launched: { 436 | process in 437 | self.currentJob = process 438 | if let show = self.actionShowsConsole, show { 439 | self.showJobConsole() 440 | } 441 | }, completed: { 442 | result in 443 | 444 | print("Result: \(result)") 445 | }) 446 | } 447 | } 448 | 449 | /// Initialize title bar 450 | func initializeTitle() { 451 | // 1. Set initial title. 452 | if let title = self.title { 453 | self.updateTitle(title: title) 454 | } 455 | 456 | // 2. Dispatch script for other title. 457 | if let titleScript = self.titleScript { 458 | titleScript.execute { [weak self] _, result in 459 | self?.menuItem?.title = result 460 | } 461 | 462 | // While we're executing the title script, 463 | // Also set up a timer if we need it. 464 | if let titleRefreshInterval = self.titleRefreshInterval { 465 | self.refreshTimer = Timer.scheduledTimer(withTimeInterval: titleRefreshInterval, repeats: true, block: { 466 | timer in 467 | 468 | // In the future, execute the title updates 469 | titleScript.execute { [weak self] _, result in 470 | // Update in background. 471 | self?.menuItem?.title = result 472 | print(result) 473 | // Invalidate if self is no longer available (gc) 474 | if self == nil { 475 | timer.invalidate() 476 | } 477 | } 478 | }) 479 | } 480 | } 481 | } 482 | } 483 | -------------------------------------------------------------------------------- /shbar2/ItemConfigMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemConfigMode.swift 3 | // shbar2 4 | // 5 | // Created by Rich Infante on 2/18/19. 6 | // Copyright © 2019 Rich Infante. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum ItemConfigMode : String, Codable { 12 | /// Refreshes contents with output of script. 13 | case RefreshingItem 14 | 15 | /// Displays job name / status icon. 16 | /// Contains submenu of launch actions. 17 | case JobStatus 18 | 19 | /// Quit the app. 20 | case ApplicationQuit 21 | } 22 | -------------------------------------------------------------------------------- /shbar2/JobStatus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JobStatus.swift 3 | // shbar2 4 | // 5 | // Created by Rich Infante on 2/18/19. 6 | // Copyright © 2019 Rich Infante. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum JobStatus : String { 12 | /// The job is suspended. 13 | case Stopped 14 | 15 | /// The job is exited. 16 | case Exited 17 | 18 | /// The job is running 19 | case Running 20 | } 21 | -------------------------------------------------------------------------------- /shbar2/LabelProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LabelProtocol.swift 3 | // shbar2 4 | // 5 | // Created by Rich Infante on 2/19/19. 6 | // Copyright © 2019 Rich Infante. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Foundation 11 | 12 | /// Provides a generic wrapper around various menu and label types 13 | protocol LabelProtocol { 14 | var title : String { get set } 15 | var attributedTitleString: NSAttributedString? { get set } 16 | var allowsNewlines : Bool { get } 17 | } 18 | 19 | /// Add conformanse for NSMenuItem 20 | extension NSMenuItem : LabelProtocol { 21 | var allowsNewlines : Bool { return false } 22 | var attributedTitleString: NSAttributedString? { 23 | get { 24 | return self.attributedTitle 25 | } 26 | set { 27 | self.attributedTitle = newValue 28 | } 29 | } 30 | } 31 | 32 | /// Add conformanse for NSStatusBarButton 33 | extension NSStatusBarButton : LabelProtocol { 34 | var allowsNewlines : Bool { return false } 35 | var attributedTitleString: NSAttributedString? { 36 | get { 37 | return self.attributedTitle 38 | } 39 | set { 40 | if let newValue = newValue { 41 | self.attributedTitle = newValue 42 | } 43 | } 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /shbar2/Script.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Script.swift 3 | // shbar2 4 | // 5 | // Created by Rich Infante on 2/18/19. 6 | // Copyright © 2019 Rich Infante. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Wrapper for process 12 | class Script : Codable { 13 | /// Absolute path to binary 14 | var bin: String 15 | 16 | /// Arguments to binary 17 | var args: [String] 18 | 19 | /// Environment variables 20 | var env: [String:String] 21 | 22 | // Pipe output from the process 23 | var uuid: UUID? 24 | var process: Process? 25 | 26 | private enum CodingKeys: String, CodingKey { 27 | case bin 28 | case args 29 | case env 30 | } 31 | 32 | /// Intialize a new script 33 | init (bin: String, args:[String], env: [String:String]) { 34 | self.bin = bin 35 | self.args = args 36 | self.env = env 37 | } 38 | 39 | /// Launch a job. 40 | func launchJob(launched: ((Process)->())? = nil, completed: ((Int32)->())? = nil) { 41 | DispatchQueue.global(qos: .background).async { [weak self] in 42 | guard let `self` = self else { return } 43 | 44 | // Create a process instance 45 | let task = Process() 46 | self.process = task 47 | 48 | // Assign a uuid to this script if it is not set yet 49 | if self.uuid == nil { 50 | let uuid = UUID() 51 | self.uuid = uuid 52 | } 53 | 54 | // Set logfile path 55 | let logfile = "\(AppDelegate.userHomeDirectoryPath)/Library/Logs/shbar/\(self.uuid!.uuidString).log" 56 | 57 | // Set process options 58 | task.currentDirectoryPath = "/" 59 | task.environment = self.env 60 | task.launchPath = self.bin 61 | task.arguments = self.args 62 | 63 | // Try to attach the logfile to the process STDOUT/STDERR 64 | do { 65 | print("Create log for \(self.bin) launch: \(logfile)") 66 | FileManager.default.createFile(atPath: logfile, contents: nil) 67 | let handle = try FileHandle(forWritingTo: URL(fileURLWithPath: logfile)) 68 | task.standardOutput = handle 69 | task.standardError = handle 70 | } catch let error { 71 | print("Error creating logfile: \(error)") 72 | } 73 | 74 | // Launch the task 75 | task.launch() 76 | 77 | // Notify of laynch 78 | DispatchQueue.main.sync { 79 | launched?(task) 80 | } 81 | 82 | print("wait for exit: \(self.bin)") 83 | 84 | // Grab the file handle so we can log when it dies. 85 | let handle: FileHandle? = task.standardOutput as? FileHandle 86 | task.waitUntilExit() 87 | 88 | // Write status to handle if it exists. 89 | if let handle = handle, let data = "exited: \(task.terminationStatus)".data(using: .utf8) { 90 | handle.write(data) 91 | } 92 | 93 | // Log status and notify callback 94 | print("Child process exited with code \(task.terminationStatus)") 95 | DispatchQueue.main.sync { 96 | completed?(task.terminationStatus) 97 | } 98 | } 99 | } 100 | 101 | 102 | /// Execute script in background, collecting output 103 | func execute(launched: ((Process)->())? = nil, completed: ((Int32, String)->())? = nil) { 104 | DispatchQueue.global(qos: .background).async { [weak self] in 105 | guard let `self` = self else { return } 106 | 107 | let pipe = Pipe() 108 | let task = Process() 109 | self.process = task 110 | if self.uuid == nil { 111 | let uuid = UUID() 112 | self.uuid = uuid 113 | } 114 | 115 | let errLogPath = "\(AppDelegate.userHomeDirectoryPath)/Library/Logs/shbar/\(self.uuid!.uuidString).log" 116 | 117 | task.currentDirectoryPath = "/" 118 | task.environment = self.env 119 | task.launchPath = self.bin 120 | task.arguments = self.args 121 | print(self.bin, self.env, self.args) 122 | task.standardOutput = pipe 123 | do { 124 | FileManager.default.createFile(atPath: errLogPath, contents: nil) 125 | print("Create log for \(self.bin) launch: \(errLogPath)") 126 | let handle2 = try FileHandle(forWritingTo: URL(fileURLWithPath: errLogPath)) 127 | task.standardError = handle2 128 | } catch let error { 129 | print("Error creating config directory: \(error)") 130 | } 131 | task.launch() 132 | DispatchQueue.main.sync { 133 | launched?(task) 134 | } 135 | task.waitUntilExit() 136 | let status = task.terminationStatus 137 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 138 | let output: String = NSString(data: data, encoding: String.Encoding.utf8.rawValue) as! String 139 | print("Child process exited with code \(status)") 140 | DispatchQueue.main.sync { 141 | completed?(status, output) 142 | } 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /shbar2/shbar2.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /shbar2Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /shbar2Tests/shbar2Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // shbar2Tests.swift 3 | // shbar2Tests 4 | // 5 | // Created by Rich Infante on 2/14/19. 6 | // Copyright © 2019 Rich Infante. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import shbar2 11 | 12 | class shbar2Tests: XCTestCase { 13 | 14 | override func setUp() { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testExample() { 23 | // This is an example of a functional test case. 24 | // Use XCTAssert and related functions to verify your tests produce the correct results. 25 | } 26 | 27 | func testPerformanceExample() { 28 | // This is an example of a performance test case. 29 | self.measure { 30 | // Put the code you want to measure the time of here. 31 | } 32 | } 33 | 34 | } 35 | --------------------------------------------------------------------------------