├── README.md ├── Today Scripts.xcodeproj └── project.pbxproj ├── Today Scripts ├── AppDelegate.h ├── AppDelegate.m ├── AppIcon.icns ├── Icon Credit.txt ├── Info.plist ├── MainMenu.xib └── main.m ├── TodayScripts.h ├── TodayScripts.m ├── Widget ├── AMR_ANSIEscapeHelper │ ├── AMR_ANSIEscapeHelper.h │ ├── AMR_ANSIEscapeHelper.m │ └── readme.txt ├── EditViewController.h ├── EditViewController.m ├── EditViewController.xib ├── Images.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist ├── ListRowViewController.h ├── ListRowViewController.m ├── ListRowViewController.xib ├── SearchViewController.h ├── SearchViewController.m ├── SearchViewController.xib ├── TodayScript.h ├── TodayScript.m ├── TodayViewController.h ├── TodayViewController.m ├── TodayViewController.xib ├── Widget.entitlements └── en.lproj │ └── InfoPlist.strings └── XPC ├── Info.plist ├── XPCHelper.m ├── XPCMain.h └── XPCMain.m /README.md: -------------------------------------------------------------------------------- 1 | #Today Scripts 2 | 3 | A widget for running scripts in the Today View in OS X Yosemite's Notification Center. 4 | 5 | [**Download the latest build here.**](https://github.com/SamRothCA/Today-Scripts/releases) 6 | 7 | [See the wiki for a list of example scripts.](https://github.com/SamRothCA/Today-Scripts/wiki) 8 | 9 | ##Features 10 | 11 | * [Colorized Output](http://i.imgur.com/Yvj2ePG.png). Today Scripts supports colorized terminal output from your scripts, as well as bold and underline. 12 | * [Custom Labels](http://i.imgur.com/LL4s6Ao.png). Today Scripts has a form for setting up scripts, which gives you the option of picking a label to display instead of the script itself. 13 | * Custom Interpreters: When setting up a script, you may specify any program to run in place of your shell. This means you can directly run scripts in Python, Perl, AppleScript, etcetera, simply by specifying their associated interpreter. 14 | * Manually Run Scripts: Scripts may be run on command by clicking on their label. You may also specify that scripts not be run automatically when Notification Center is opened. 15 | * Output text selection: You may highlight the output of your scripts, allowing you to copy it to the clipboard or drag it where you please. 16 | 17 | ##Usage 18 | 19 | After building, simply copy "Today Scripts.app" wherever you'd like to store it, then open it. In Notification Center, you will see "1 New" appear on the edit button, and you may use that to add Today Scripts to your Today View in order to begin using it. 20 | 21 | To begin editing your list of scripts, click the "Info" symbol in the title of the widget. 22 | 23 | To start or stop a given script on demand, click its label in your list. 24 | 25 | To edit an existing script, click the "action" button to the right of its label. 26 | 27 | ##Technical Details 28 | 29 | * An interpreter can be speficied using a path to any valid executable (it need not be an "interpreter" at all). The provided script is piped to the interpreter via its standard input. 30 | * Today Scripts emulates a 40-column terminal. When running a script, a pseudo-TTY is opened for it, and the standard output and standard error of it is set to that. The `COLUMNS` environment variable for scripts is set to `40`, and `PAGER` is set to `/bin/cat`. 31 | * Today Scripts supports all ANSI color sequences; both standard and bright, as well as both foreground and background. The `TERM` environment variable for scripts is set to `ansi`. 32 | 33 | ##Special Thanks 34 | 35 | Support for ANSI escape sequences made possible thanks to [Ali Rantakari's ANSIEscapeHelper](http://hasseg.org/ansiEscapeHelper/). 36 | 37 | Icon designed by [Friedrich Preuß](http://phriedrich.de). 38 | -------------------------------------------------------------------------------- /Today Scripts.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 223F9A0A19F894AF000802FB /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 223F9A0919F894AF000802FB /* AppDelegate.m */; }; 11 | 223F9A0C19F894AF000802FB /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 223F9A0B19F894AF000802FB /* main.m */; }; 12 | 223F9A2D19F894DB000802FB /* NotificationCenter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 223F9A2C19F894DB000802FB /* NotificationCenter.framework */; }; 13 | 223F9A3419F894DB000802FB /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 223F9A3219F894DB000802FB /* InfoPlist.strings */; }; 14 | 223F9A4119F894DB000802FB /* Widget.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 223F9A2A19F894DB000802FB /* Widget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 15 | 223F9A5D19F895AF000802FB /* TodayScripts.m in Sources */ = {isa = PBXBuildFile; fileRef = 223F9A5C19F895AF000802FB /* TodayScripts.m */; }; 16 | 223F9A5E19F895AF000802FB /* TodayScripts.m in Sources */ = {isa = PBXBuildFile; fileRef = 223F9A5C19F895AF000802FB /* TodayScripts.m */; }; 17 | 223F9A6C19F8963C000802FB /* AMR_ANSIEscapeHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 223F9A6019F8963C000802FB /* AMR_ANSIEscapeHelper.m */; }; 18 | 223F9A6D19F8963C000802FB /* ListRowViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 223F9A6219F8963C000802FB /* ListRowViewController.m */; }; 19 | 223F9A6E19F8963C000802FB /* ListRowViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 223F9A6319F8963C000802FB /* ListRowViewController.xib */; }; 20 | 223F9A6F19F8963C000802FB /* EditViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 223F9A6519F8963C000802FB /* EditViewController.m */; }; 21 | 223F9A7019F8963C000802FB /* EditViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 223F9A6619F8963C000802FB /* EditViewController.xib */; }; 22 | 223F9A7119F8963C000802FB /* TodayScript.m in Sources */ = {isa = PBXBuildFile; fileRef = 223F9A6819F8963C000802FB /* TodayScript.m */; }; 23 | 223F9A7219F8963C000802FB /* TodayViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 223F9A6A19F8963C000802FB /* TodayViewController.m */; }; 24 | 223F9A7319F8963C000802FB /* TodayViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 223F9A6B19F8963C000802FB /* TodayViewController.xib */; }; 25 | 223F9A7719F896BF000802FB /* XPCHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 223F9A7419F896BF000802FB /* XPCHelper.m */; }; 26 | 223F9A7819F896BF000802FB /* XPCMain.m in Sources */ = {isa = PBXBuildFile; fileRef = 223F9A7619F896BF000802FB /* XPCMain.m */; }; 27 | 2265B37619FC519C000802FB /* AppIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 2265B37519FC519C000802FB /* AppIcon.icns */; }; 28 | 2265B37819FC51DF000802FB /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2265B37719FC51DF000802FB /* Images.xcassets */; }; 29 | 2265CC7319F89D18000802FB /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2265CC7219F89D18000802FB /* MainMenu.xib */; }; 30 | 2265CC7619F89D86000802FB /* XPC.xpc in CopyFiles */ = {isa = PBXBuildFile; fileRef = 223F9A4A19F89532000802FB /* XPC.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 31 | 22A1BFCD1A004D8C000802FB /* Icon Credit.txt in Resources */ = {isa = PBXBuildFile; fileRef = 22A1BFCC1A004D8C000802FB /* Icon Credit.txt */; }; 32 | /* End PBXBuildFile section */ 33 | 34 | /* Begin PBXContainerItemProxy section */ 35 | 223F9A3F19F894DB000802FB /* PBXContainerItemProxy */ = { 36 | isa = PBXContainerItemProxy; 37 | containerPortal = 223F99FB19F894AE000802FB /* Project object */; 38 | proxyType = 1; 39 | remoteGlobalIDString = 223F9A2919F894DB000802FB; 40 | remoteInfo = Widget; 41 | }; 42 | 223F9A5419F89532000802FB /* PBXContainerItemProxy */ = { 43 | isa = PBXContainerItemProxy; 44 | containerPortal = 223F99FB19F894AE000802FB /* Project object */; 45 | proxyType = 1; 46 | remoteGlobalIDString = 223F9A4919F89532000802FB; 47 | remoteInfo = XPC; 48 | }; 49 | 223F9A7919F89745000802FB /* PBXContainerItemProxy */ = { 50 | isa = PBXContainerItemProxy; 51 | containerPortal = 223F99FB19F894AE000802FB /* Project object */; 52 | proxyType = 1; 53 | remoteGlobalIDString = 223F9A4919F89532000802FB; 54 | remoteInfo = XPC; 55 | }; 56 | /* End PBXContainerItemProxy section */ 57 | 58 | /* Begin PBXCopyFilesBuildPhase section */ 59 | 223F9A4519F894DB000802FB /* Embed App Extensions */ = { 60 | isa = PBXCopyFilesBuildPhase; 61 | buildActionMask = 2147483647; 62 | dstPath = ""; 63 | dstSubfolderSpec = 13; 64 | files = ( 65 | 223F9A4119F894DB000802FB /* Widget.appex in Embed App Extensions */, 66 | ); 67 | name = "Embed App Extensions"; 68 | runOnlyForDeploymentPostprocessing = 0; 69 | }; 70 | 223F9A5A19F89532000802FB /* Embed XPC Services */ = { 71 | isa = PBXCopyFilesBuildPhase; 72 | buildActionMask = 2147483647; 73 | dstPath = "$(CONTENTS_FOLDER_PATH)/XPCServices"; 74 | dstSubfolderSpec = 16; 75 | files = ( 76 | ); 77 | name = "Embed XPC Services"; 78 | runOnlyForDeploymentPostprocessing = 0; 79 | }; 80 | 2265CC7519F89D7F000802FB /* CopyFiles */ = { 81 | isa = PBXCopyFilesBuildPhase; 82 | buildActionMask = 2147483647; 83 | dstPath = "$(CONTENTS_FOLDER_PATH)/XPCServices"; 84 | dstSubfolderSpec = 16; 85 | files = ( 86 | 2265CC7619F89D86000802FB /* XPC.xpc in CopyFiles */, 87 | ); 88 | runOnlyForDeploymentPostprocessing = 0; 89 | }; 90 | /* End PBXCopyFilesBuildPhase section */ 91 | 92 | /* Begin PBXFileReference section */ 93 | 22211C9F1A0317EB000802FB /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 94 | 223F9A0319F894AE000802FB /* Today Scripts.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Today Scripts.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 95 | 223F9A0719F894AF000802FB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 96 | 223F9A0819F894AF000802FB /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 97 | 223F9A0919F894AF000802FB /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 98 | 223F9A0B19F894AF000802FB /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 99 | 223F9A2A19F894DB000802FB /* Widget.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = Widget.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 100 | 223F9A2C19F894DB000802FB /* NotificationCenter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NotificationCenter.framework; path = System/Library/Frameworks/NotificationCenter.framework; sourceTree = SDKROOT; }; 101 | 223F9A3019F894DB000802FB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 102 | 223F9A3119F894DB000802FB /* Widget.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Widget.entitlements; sourceTree = ""; }; 103 | 223F9A3319F894DB000802FB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 104 | 223F9A4A19F89532000802FB /* XPC.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = XPC.xpc; sourceTree = BUILT_PRODUCTS_DIR; }; 105 | 223F9A4D19F89532000802FB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 106 | 223F9A5B19F895AF000802FB /* TodayScripts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TodayScripts.h; sourceTree = ""; }; 107 | 223F9A5C19F895AF000802FB /* TodayScripts.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TodayScripts.m; sourceTree = ""; }; 108 | 223F9A5F19F8963C000802FB /* AMR_ANSIEscapeHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AMR_ANSIEscapeHelper.h; sourceTree = ""; }; 109 | 223F9A6019F8963C000802FB /* AMR_ANSIEscapeHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AMR_ANSIEscapeHelper.m; sourceTree = ""; }; 110 | 223F9A6119F8963C000802FB /* ListRowViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ListRowViewController.h; sourceTree = ""; }; 111 | 223F9A6219F8963C000802FB /* ListRowViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ListRowViewController.m; sourceTree = ""; }; 112 | 223F9A6319F8963C000802FB /* ListRowViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ListRowViewController.xib; sourceTree = ""; }; 113 | 223F9A6419F8963C000802FB /* EditViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EditViewController.h; sourceTree = ""; }; 114 | 223F9A6519F8963C000802FB /* EditViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EditViewController.m; sourceTree = ""; }; 115 | 223F9A6619F8963C000802FB /* EditViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = EditViewController.xib; sourceTree = ""; }; 116 | 223F9A6719F8963C000802FB /* TodayScript.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TodayScript.h; sourceTree = ""; }; 117 | 223F9A6819F8963C000802FB /* TodayScript.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TodayScript.m; sourceTree = ""; }; 118 | 223F9A6919F8963C000802FB /* TodayViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TodayViewController.h; sourceTree = ""; }; 119 | 223F9A6A19F8963C000802FB /* TodayViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TodayViewController.m; sourceTree = ""; }; 120 | 223F9A6B19F8963C000802FB /* TodayViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TodayViewController.xib; sourceTree = ""; }; 121 | 223F9A7419F896BF000802FB /* XPCHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XPCHelper.m; sourceTree = ""; }; 122 | 223F9A7519F896BF000802FB /* XPCMain.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XPCMain.h; sourceTree = ""; }; 123 | 223F9A7619F896BF000802FB /* XPCMain.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XPCMain.m; sourceTree = ""; }; 124 | 2265B37519FC519C000802FB /* AppIcon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = AppIcon.icns; sourceTree = ""; }; 125 | 2265B37719FC51DF000802FB /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 126 | 2265B37A19FC53CA000802FB /* readme.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = readme.txt; sourceTree = ""; }; 127 | 2265CC7219F89D18000802FB /* MainMenu.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; }; 128 | 22A1BFCC1A004D8C000802FB /* Icon Credit.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "Icon Credit.txt"; sourceTree = ""; }; 129 | /* End PBXFileReference section */ 130 | 131 | /* Begin PBXFrameworksBuildPhase section */ 132 | 223F9A0019F894AE000802FB /* Frameworks */ = { 133 | isa = PBXFrameworksBuildPhase; 134 | buildActionMask = 2147483647; 135 | files = ( 136 | ); 137 | runOnlyForDeploymentPostprocessing = 0; 138 | }; 139 | 223F9A2719F894DB000802FB /* Frameworks */ = { 140 | isa = PBXFrameworksBuildPhase; 141 | buildActionMask = 2147483647; 142 | files = ( 143 | 223F9A2D19F894DB000802FB /* NotificationCenter.framework in Frameworks */, 144 | ); 145 | runOnlyForDeploymentPostprocessing = 0; 146 | }; 147 | 223F9A4719F89532000802FB /* Frameworks */ = { 148 | isa = PBXFrameworksBuildPhase; 149 | buildActionMask = 2147483647; 150 | files = ( 151 | ); 152 | runOnlyForDeploymentPostprocessing = 0; 153 | }; 154 | /* End PBXFrameworksBuildPhase section */ 155 | 156 | /* Begin PBXGroup section */ 157 | 223F99FA19F894AE000802FB = { 158 | isa = PBXGroup; 159 | children = ( 160 | 22211C9F1A0317EB000802FB /* README.md */, 161 | 223F9A5B19F895AF000802FB /* TodayScripts.h */, 162 | 223F9A5C19F895AF000802FB /* TodayScripts.m */, 163 | 223F9A0519F894AE000802FB /* Today Scripts */, 164 | 223F9A2E19F894DB000802FB /* Widget */, 165 | 223F9A4B19F89532000802FB /* XPC */, 166 | 223F9A2B19F894DB000802FB /* Frameworks */, 167 | 223F9A0419F894AE000802FB /* Products */, 168 | ); 169 | sourceTree = ""; 170 | }; 171 | 223F9A0419F894AE000802FB /* Products */ = { 172 | isa = PBXGroup; 173 | children = ( 174 | 223F9A0319F894AE000802FB /* Today Scripts.app */, 175 | 223F9A2A19F894DB000802FB /* Widget.appex */, 176 | 223F9A4A19F89532000802FB /* XPC.xpc */, 177 | ); 178 | name = Products; 179 | sourceTree = ""; 180 | }; 181 | 223F9A0519F894AE000802FB /* Today Scripts */ = { 182 | isa = PBXGroup; 183 | children = ( 184 | 223F9A0819F894AF000802FB /* AppDelegate.h */, 185 | 223F9A0919F894AF000802FB /* AppDelegate.m */, 186 | 2265CC7219F89D18000802FB /* MainMenu.xib */, 187 | 223F9A0619F894AF000802FB /* Supporting Files */, 188 | ); 189 | path = "Today Scripts"; 190 | sourceTree = ""; 191 | }; 192 | 223F9A0619F894AF000802FB /* Supporting Files */ = { 193 | isa = PBXGroup; 194 | children = ( 195 | 22A1BFCC1A004D8C000802FB /* Icon Credit.txt */, 196 | 2265B37519FC519C000802FB /* AppIcon.icns */, 197 | 223F9A0719F894AF000802FB /* Info.plist */, 198 | 223F9A0B19F894AF000802FB /* main.m */, 199 | ); 200 | name = "Supporting Files"; 201 | sourceTree = ""; 202 | }; 203 | 223F9A2B19F894DB000802FB /* Frameworks */ = { 204 | isa = PBXGroup; 205 | children = ( 206 | 223F9A2C19F894DB000802FB /* NotificationCenter.framework */, 207 | ); 208 | name = Frameworks; 209 | sourceTree = ""; 210 | }; 211 | 223F9A2E19F894DB000802FB /* Widget */ = { 212 | isa = PBXGroup; 213 | children = ( 214 | 223F9A6719F8963C000802FB /* TodayScript.h */, 215 | 223F9A6819F8963C000802FB /* TodayScript.m */, 216 | 223F9A6919F8963C000802FB /* TodayViewController.h */, 217 | 223F9A6A19F8963C000802FB /* TodayViewController.m */, 218 | 223F9A6B19F8963C000802FB /* TodayViewController.xib */, 219 | 223F9A6119F8963C000802FB /* ListRowViewController.h */, 220 | 223F9A6219F8963C000802FB /* ListRowViewController.m */, 221 | 223F9A6319F8963C000802FB /* ListRowViewController.xib */, 222 | 223F9A6419F8963C000802FB /* EditViewController.h */, 223 | 223F9A6519F8963C000802FB /* EditViewController.m */, 224 | 223F9A6619F8963C000802FB /* EditViewController.xib */, 225 | 2265B37919FC539E000802FB /* AMR_ANSIEscapeHelper */, 226 | 223F9A2F19F894DB000802FB /* Supporting Files */, 227 | ); 228 | path = Widget; 229 | sourceTree = ""; 230 | }; 231 | 223F9A2F19F894DB000802FB /* Supporting Files */ = { 232 | isa = PBXGroup; 233 | children = ( 234 | 2265B37719FC51DF000802FB /* Images.xcassets */, 235 | 223F9A3019F894DB000802FB /* Info.plist */, 236 | 223F9A3119F894DB000802FB /* Widget.entitlements */, 237 | 223F9A3219F894DB000802FB /* InfoPlist.strings */, 238 | ); 239 | name = "Supporting Files"; 240 | sourceTree = ""; 241 | }; 242 | 223F9A4B19F89532000802FB /* XPC */ = { 243 | isa = PBXGroup; 244 | children = ( 245 | 223F9A7519F896BF000802FB /* XPCMain.h */, 246 | 223F9A7619F896BF000802FB /* XPCMain.m */, 247 | 223F9A7419F896BF000802FB /* XPCHelper.m */, 248 | 223F9A4C19F89532000802FB /* Supporting Files */, 249 | ); 250 | path = XPC; 251 | sourceTree = ""; 252 | }; 253 | 223F9A4C19F89532000802FB /* Supporting Files */ = { 254 | isa = PBXGroup; 255 | children = ( 256 | 223F9A4D19F89532000802FB /* Info.plist */, 257 | ); 258 | name = "Supporting Files"; 259 | sourceTree = ""; 260 | }; 261 | 2265B37919FC539E000802FB /* AMR_ANSIEscapeHelper */ = { 262 | isa = PBXGroup; 263 | children = ( 264 | 2265B37A19FC53CA000802FB /* readme.txt */, 265 | 223F9A5F19F8963C000802FB /* AMR_ANSIEscapeHelper.h */, 266 | 223F9A6019F8963C000802FB /* AMR_ANSIEscapeHelper.m */, 267 | ); 268 | path = AMR_ANSIEscapeHelper; 269 | sourceTree = ""; 270 | }; 271 | /* End PBXGroup section */ 272 | 273 | /* Begin PBXNativeTarget section */ 274 | 223F9A0219F894AE000802FB /* Today Scripts */ = { 275 | isa = PBXNativeTarget; 276 | buildConfigurationList = 223F9A2019F894AF000802FB /* Build configuration list for PBXNativeTarget "Today Scripts" */; 277 | buildPhases = ( 278 | 223F99FF19F894AE000802FB /* Sources */, 279 | 223F9A0019F894AE000802FB /* Frameworks */, 280 | 223F9A0119F894AE000802FB /* Resources */, 281 | 223F9A4519F894DB000802FB /* Embed App Extensions */, 282 | 223F9A5A19F89532000802FB /* Embed XPC Services */, 283 | ); 284 | buildRules = ( 285 | ); 286 | dependencies = ( 287 | 223F9A4019F894DB000802FB /* PBXTargetDependency */, 288 | 223F9A5519F89532000802FB /* PBXTargetDependency */, 289 | ); 290 | name = "Today Scripts"; 291 | productName = "Today Scripts"; 292 | productReference = 223F9A0319F894AE000802FB /* Today Scripts.app */; 293 | productType = "com.apple.product-type.application"; 294 | }; 295 | 223F9A2919F894DB000802FB /* Widget */ = { 296 | isa = PBXNativeTarget; 297 | buildConfigurationList = 223F9A4219F894DB000802FB /* Build configuration list for PBXNativeTarget "Widget" */; 298 | buildPhases = ( 299 | 223F9A2619F894DB000802FB /* Sources */, 300 | 223F9A2719F894DB000802FB /* Frameworks */, 301 | 223F9A2819F894DB000802FB /* Resources */, 302 | 2265CC7519F89D7F000802FB /* CopyFiles */, 303 | ); 304 | buildRules = ( 305 | ); 306 | dependencies = ( 307 | 223F9A7A19F89745000802FB /* PBXTargetDependency */, 308 | ); 309 | name = Widget; 310 | productName = Widget; 311 | productReference = 223F9A2A19F894DB000802FB /* Widget.appex */; 312 | productType = "com.apple.product-type.app-extension"; 313 | }; 314 | 223F9A4919F89532000802FB /* XPC */ = { 315 | isa = PBXNativeTarget; 316 | buildConfigurationList = 223F9A5719F89532000802FB /* Build configuration list for PBXNativeTarget "XPC" */; 317 | buildPhases = ( 318 | 223F9A4619F89532000802FB /* Sources */, 319 | 223F9A4719F89532000802FB /* Frameworks */, 320 | 223F9A4819F89532000802FB /* Resources */, 321 | ); 322 | buildRules = ( 323 | ); 324 | dependencies = ( 325 | ); 326 | name = XPC; 327 | productName = XPC; 328 | productReference = 223F9A4A19F89532000802FB /* XPC.xpc */; 329 | productType = "com.apple.product-type.xpc-service"; 330 | }; 331 | /* End PBXNativeTarget section */ 332 | 333 | /* Begin PBXProject section */ 334 | 223F99FB19F894AE000802FB /* Project object */ = { 335 | isa = PBXProject; 336 | attributes = { 337 | LastUpgradeCheck = 0610; 338 | ORGANIZATIONNAME = "Sam Rothenberg"; 339 | TargetAttributes = { 340 | 223F9A0219F894AE000802FB = { 341 | CreatedOnToolsVersion = 6.1; 342 | }; 343 | 223F9A2919F894DB000802FB = { 344 | CreatedOnToolsVersion = 6.1; 345 | }; 346 | 223F9A4919F89532000802FB = { 347 | CreatedOnToolsVersion = 6.1; 348 | SystemCapabilities = { 349 | com.apple.Sandbox = { 350 | enabled = 0; 351 | }; 352 | }; 353 | }; 354 | }; 355 | }; 356 | buildConfigurationList = 223F99FE19F894AE000802FB /* Build configuration list for PBXProject "Today Scripts" */; 357 | compatibilityVersion = "Xcode 3.2"; 358 | developmentRegion = English; 359 | hasScannedForEncodings = 0; 360 | knownRegions = ( 361 | en, 362 | Base, 363 | ); 364 | mainGroup = 223F99FA19F894AE000802FB; 365 | productRefGroup = 223F9A0419F894AE000802FB /* Products */; 366 | projectDirPath = ""; 367 | projectRoot = ""; 368 | targets = ( 369 | 223F9A0219F894AE000802FB /* Today Scripts */, 370 | 223F9A2919F894DB000802FB /* Widget */, 371 | 223F9A4919F89532000802FB /* XPC */, 372 | ); 373 | }; 374 | /* End PBXProject section */ 375 | 376 | /* Begin PBXResourcesBuildPhase section */ 377 | 223F9A0119F894AE000802FB /* Resources */ = { 378 | isa = PBXResourcesBuildPhase; 379 | buildActionMask = 2147483647; 380 | files = ( 381 | 2265CC7319F89D18000802FB /* MainMenu.xib in Resources */, 382 | 2265B37619FC519C000802FB /* AppIcon.icns in Resources */, 383 | 22A1BFCD1A004D8C000802FB /* Icon Credit.txt in Resources */, 384 | ); 385 | runOnlyForDeploymentPostprocessing = 0; 386 | }; 387 | 223F9A2819F894DB000802FB /* Resources */ = { 388 | isa = PBXResourcesBuildPhase; 389 | buildActionMask = 2147483647; 390 | files = ( 391 | 223F9A6E19F8963C000802FB /* ListRowViewController.xib in Resources */, 392 | 2265B37819FC51DF000802FB /* Images.xcassets in Resources */, 393 | 223F9A7319F8963C000802FB /* TodayViewController.xib in Resources */, 394 | 223F9A7019F8963C000802FB /* EditViewController.xib in Resources */, 395 | 223F9A3419F894DB000802FB /* InfoPlist.strings in Resources */, 396 | ); 397 | runOnlyForDeploymentPostprocessing = 0; 398 | }; 399 | 223F9A4819F89532000802FB /* Resources */ = { 400 | isa = PBXResourcesBuildPhase; 401 | buildActionMask = 2147483647; 402 | files = ( 403 | ); 404 | runOnlyForDeploymentPostprocessing = 0; 405 | }; 406 | /* End PBXResourcesBuildPhase section */ 407 | 408 | /* Begin PBXSourcesBuildPhase section */ 409 | 223F99FF19F894AE000802FB /* Sources */ = { 410 | isa = PBXSourcesBuildPhase; 411 | buildActionMask = 2147483647; 412 | files = ( 413 | 223F9A0C19F894AF000802FB /* main.m in Sources */, 414 | 223F9A0A19F894AF000802FB /* AppDelegate.m in Sources */, 415 | ); 416 | runOnlyForDeploymentPostprocessing = 0; 417 | }; 418 | 223F9A2619F894DB000802FB /* Sources */ = { 419 | isa = PBXSourcesBuildPhase; 420 | buildActionMask = 2147483647; 421 | files = ( 422 | 223F9A7219F8963C000802FB /* TodayViewController.m in Sources */, 423 | 223F9A6C19F8963C000802FB /* AMR_ANSIEscapeHelper.m in Sources */, 424 | 223F9A5D19F895AF000802FB /* TodayScripts.m in Sources */, 425 | 223F9A6D19F8963C000802FB /* ListRowViewController.m in Sources */, 426 | 223F9A6F19F8963C000802FB /* EditViewController.m in Sources */, 427 | 223F9A7119F8963C000802FB /* TodayScript.m in Sources */, 428 | ); 429 | runOnlyForDeploymentPostprocessing = 0; 430 | }; 431 | 223F9A4619F89532000802FB /* Sources */ = { 432 | isa = PBXSourcesBuildPhase; 433 | buildActionMask = 2147483647; 434 | files = ( 435 | 223F9A5E19F895AF000802FB /* TodayScripts.m in Sources */, 436 | 223F9A7819F896BF000802FB /* XPCMain.m in Sources */, 437 | 223F9A7719F896BF000802FB /* XPCHelper.m in Sources */, 438 | ); 439 | runOnlyForDeploymentPostprocessing = 0; 440 | }; 441 | /* End PBXSourcesBuildPhase section */ 442 | 443 | /* Begin PBXTargetDependency section */ 444 | 223F9A4019F894DB000802FB /* PBXTargetDependency */ = { 445 | isa = PBXTargetDependency; 446 | target = 223F9A2919F894DB000802FB /* Widget */; 447 | targetProxy = 223F9A3F19F894DB000802FB /* PBXContainerItemProxy */; 448 | }; 449 | 223F9A5519F89532000802FB /* PBXTargetDependency */ = { 450 | isa = PBXTargetDependency; 451 | target = 223F9A4919F89532000802FB /* XPC */; 452 | targetProxy = 223F9A5419F89532000802FB /* PBXContainerItemProxy */; 453 | }; 454 | 223F9A7A19F89745000802FB /* PBXTargetDependency */ = { 455 | isa = PBXTargetDependency; 456 | target = 223F9A4919F89532000802FB /* XPC */; 457 | targetProxy = 223F9A7919F89745000802FB /* PBXContainerItemProxy */; 458 | }; 459 | /* End PBXTargetDependency section */ 460 | 461 | /* Begin PBXVariantGroup section */ 462 | 223F9A3219F894DB000802FB /* InfoPlist.strings */ = { 463 | isa = PBXVariantGroup; 464 | children = ( 465 | 223F9A3319F894DB000802FB /* en */, 466 | ); 467 | name = InfoPlist.strings; 468 | sourceTree = ""; 469 | }; 470 | /* End PBXVariantGroup section */ 471 | 472 | /* Begin XCBuildConfiguration section */ 473 | 223F9A1E19F894AF000802FB /* Debug */ = { 474 | isa = XCBuildConfiguration; 475 | buildSettings = { 476 | ALWAYS_SEARCH_USER_PATHS = NO; 477 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 478 | CLANG_CXX_LIBRARY = "libc++"; 479 | CLANG_ENABLE_MODULES = YES; 480 | CLANG_ENABLE_OBJC_ARC = YES; 481 | CLANG_WARN_BOOL_CONVERSION = YES; 482 | CLANG_WARN_CONSTANT_CONVERSION = YES; 483 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 484 | CLANG_WARN_EMPTY_BODY = YES; 485 | CLANG_WARN_ENUM_CONVERSION = YES; 486 | CLANG_WARN_INT_CONVERSION = YES; 487 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 488 | CLANG_WARN_UNREACHABLE_CODE = YES; 489 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 490 | CODE_SIGN_IDENTITY = "-"; 491 | COPY_PHASE_STRIP = NO; 492 | ENABLE_STRICT_OBJC_MSGSEND = YES; 493 | GCC_C_LANGUAGE_STANDARD = gnu99; 494 | GCC_DYNAMIC_NO_PIC = NO; 495 | GCC_OPTIMIZATION_LEVEL = 0; 496 | GCC_PREPROCESSOR_DEFINITIONS = ( 497 | "DEBUG=1", 498 | "$(inherited)", 499 | ); 500 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 501 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 502 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 503 | GCC_WARN_UNDECLARED_SELECTOR = YES; 504 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 505 | GCC_WARN_UNUSED_FUNCTION = YES; 506 | GCC_WARN_UNUSED_VARIABLE = YES; 507 | MACOSX_DEPLOYMENT_TARGET = 10.10; 508 | MTL_ENABLE_DEBUG_INFO = YES; 509 | ONLY_ACTIVE_ARCH = YES; 510 | SDKROOT = macosx; 511 | }; 512 | name = Debug; 513 | }; 514 | 223F9A1F19F894AF000802FB /* Release */ = { 515 | isa = XCBuildConfiguration; 516 | buildSettings = { 517 | ALWAYS_SEARCH_USER_PATHS = NO; 518 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 519 | CLANG_CXX_LIBRARY = "libc++"; 520 | CLANG_ENABLE_MODULES = YES; 521 | CLANG_ENABLE_OBJC_ARC = YES; 522 | CLANG_WARN_BOOL_CONVERSION = YES; 523 | CLANG_WARN_CONSTANT_CONVERSION = YES; 524 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 525 | CLANG_WARN_EMPTY_BODY = YES; 526 | CLANG_WARN_ENUM_CONVERSION = YES; 527 | CLANG_WARN_INT_CONVERSION = YES; 528 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 529 | CLANG_WARN_UNREACHABLE_CODE = YES; 530 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 531 | CODE_SIGN_IDENTITY = "-"; 532 | COPY_PHASE_STRIP = YES; 533 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 534 | ENABLE_NS_ASSERTIONS = NO; 535 | ENABLE_STRICT_OBJC_MSGSEND = YES; 536 | GCC_C_LANGUAGE_STANDARD = gnu99; 537 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 538 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 539 | GCC_WARN_UNDECLARED_SELECTOR = YES; 540 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 541 | GCC_WARN_UNUSED_FUNCTION = YES; 542 | GCC_WARN_UNUSED_VARIABLE = YES; 543 | MACOSX_DEPLOYMENT_TARGET = 10.10; 544 | MTL_ENABLE_DEBUG_INFO = NO; 545 | SDKROOT = macosx; 546 | }; 547 | name = Release; 548 | }; 549 | 223F9A2119F894AF000802FB /* Debug */ = { 550 | isa = XCBuildConfiguration; 551 | buildSettings = { 552 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 553 | COMBINE_HIDPI_IMAGES = YES; 554 | INFOPLIST_FILE = "Today Scripts/Info.plist"; 555 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 556 | PRODUCT_NAME = "$(TARGET_NAME)"; 557 | }; 558 | name = Debug; 559 | }; 560 | 223F9A2219F894AF000802FB /* Release */ = { 561 | isa = XCBuildConfiguration; 562 | buildSettings = { 563 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 564 | COMBINE_HIDPI_IMAGES = YES; 565 | INFOPLIST_FILE = "Today Scripts/Info.plist"; 566 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 567 | PRODUCT_NAME = "$(TARGET_NAME)"; 568 | }; 569 | name = Release; 570 | }; 571 | 223F9A4319F894DB000802FB /* Debug */ = { 572 | isa = XCBuildConfiguration; 573 | buildSettings = { 574 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 575 | CODE_SIGN_ENTITLEMENTS = Widget/Widget.entitlements; 576 | CODE_SIGN_IDENTITY = "-"; 577 | COMBINE_HIDPI_IMAGES = YES; 578 | GCC_PREPROCESSOR_DEFINITIONS = ( 579 | "DEBUG=1", 580 | "$(inherited)", 581 | ); 582 | INFOPLIST_FILE = Widget/Info.plist; 583 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks"; 584 | PRODUCT_NAME = "$(TARGET_NAME)"; 585 | SKIP_INSTALL = YES; 586 | }; 587 | name = Debug; 588 | }; 589 | 223F9A4419F894DB000802FB /* Release */ = { 590 | isa = XCBuildConfiguration; 591 | buildSettings = { 592 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 593 | CODE_SIGN_ENTITLEMENTS = Widget/Widget.entitlements; 594 | CODE_SIGN_IDENTITY = "-"; 595 | COMBINE_HIDPI_IMAGES = YES; 596 | INFOPLIST_FILE = Widget/Info.plist; 597 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks"; 598 | PRODUCT_NAME = "$(TARGET_NAME)"; 599 | SKIP_INSTALL = YES; 600 | }; 601 | name = Release; 602 | }; 603 | 223F9A5819F89532000802FB /* Debug */ = { 604 | isa = XCBuildConfiguration; 605 | buildSettings = { 606 | COMBINE_HIDPI_IMAGES = YES; 607 | GCC_PREPROCESSOR_DEFINITIONS = ( 608 | "DEBUG=1", 609 | "$(inherited)", 610 | ); 611 | INFOPLIST_FILE = XPC/Info.plist; 612 | PRODUCT_NAME = "$(TARGET_NAME)"; 613 | SKIP_INSTALL = YES; 614 | }; 615 | name = Debug; 616 | }; 617 | 223F9A5919F89532000802FB /* Release */ = { 618 | isa = XCBuildConfiguration; 619 | buildSettings = { 620 | COMBINE_HIDPI_IMAGES = YES; 621 | INFOPLIST_FILE = XPC/Info.plist; 622 | PRODUCT_NAME = "$(TARGET_NAME)"; 623 | SKIP_INSTALL = YES; 624 | }; 625 | name = Release; 626 | }; 627 | /* End XCBuildConfiguration section */ 628 | 629 | /* Begin XCConfigurationList section */ 630 | 223F99FE19F894AE000802FB /* Build configuration list for PBXProject "Today Scripts" */ = { 631 | isa = XCConfigurationList; 632 | buildConfigurations = ( 633 | 223F9A1E19F894AF000802FB /* Debug */, 634 | 223F9A1F19F894AF000802FB /* Release */, 635 | ); 636 | defaultConfigurationIsVisible = 0; 637 | defaultConfigurationName = Release; 638 | }; 639 | 223F9A2019F894AF000802FB /* Build configuration list for PBXNativeTarget "Today Scripts" */ = { 640 | isa = XCConfigurationList; 641 | buildConfigurations = ( 642 | 223F9A2119F894AF000802FB /* Debug */, 643 | 223F9A2219F894AF000802FB /* Release */, 644 | ); 645 | defaultConfigurationIsVisible = 0; 646 | defaultConfigurationName = Release; 647 | }; 648 | 223F9A4219F894DB000802FB /* Build configuration list for PBXNativeTarget "Widget" */ = { 649 | isa = XCConfigurationList; 650 | buildConfigurations = ( 651 | 223F9A4319F894DB000802FB /* Debug */, 652 | 223F9A4419F894DB000802FB /* Release */, 653 | ); 654 | defaultConfigurationIsVisible = 0; 655 | defaultConfigurationName = Release; 656 | }; 657 | 223F9A5719F89532000802FB /* Build configuration list for PBXNativeTarget "XPC" */ = { 658 | isa = XCConfigurationList; 659 | buildConfigurations = ( 660 | 223F9A5819F89532000802FB /* Debug */, 661 | 223F9A5919F89532000802FB /* Release */, 662 | ); 663 | defaultConfigurationIsVisible = 0; 664 | defaultConfigurationName = Release; 665 | }; 666 | /* End XCConfigurationList section */ 667 | }; 668 | rootObject = 223F99FB19F894AE000802FB /* Project object */; 669 | } 670 | -------------------------------------------------------------------------------- /Today Scripts/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // Today Scripts 4 | // 5 | // Created by Sam Rothenberg on 10/22/14. 6 | // Copyright (c) 2014 Sam Rothenberg. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : NSObject 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /Today Scripts/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // Today Scripts 4 | // 5 | // Created by Sam Rothenberg on 10/22/14. 6 | // Copyright (c) 2014 Sam Rothenberg. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @interface AppDelegate () 12 | 13 | @end 14 | 15 | @implementation AppDelegate 16 | 17 | - (void)applicationWillFinishLaunching:(NSNotification *)aNotification { 18 | [NSApplication.sharedApplication terminate:self]; 19 | } 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /Today Scripts/AppIcon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SamRothCA/Today-Scripts/292993b8ff731f2f1f60125d8edab62ab1aa6bd1/Today Scripts/AppIcon.icns -------------------------------------------------------------------------------- /Today Scripts/Icon Credit.txt: -------------------------------------------------------------------------------- 1 | Icon thanks to Friedrich Preuß 2 | http://phriedrich.de -------------------------------------------------------------------------------- /Today Scripts/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | AppIcon 11 | CFBundleIdentifier 12 | org.samroth.$(PRODUCT_NAME:rfc1034identifier) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.1 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | LSUIElement 28 | 29 | NSHumanReadableCopyright 30 | Copyright © 2014 Sam Rothenberg. All rights reserved. 31 | NSMainNibFile 32 | MainMenu 33 | NSPrincipalClass 34 | NSApplication 35 | 36 | 37 | -------------------------------------------------------------------------------- /Today Scripts/MainMenu.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Today Scripts/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // Today Scripts 4 | // 5 | // Created by Sam Rothenberg on 10/22/14. 6 | // Copyright (c) 2014 Sam Rothenberg. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | int main(int argc, const char * argv[]) { 12 | return NSApplicationMain(argc, argv); 13 | } 14 | -------------------------------------------------------------------------------- /TodayScripts.h: -------------------------------------------------------------------------------- 1 | // 2 | // TodayScripts.m 3 | // Today Scripts 4 | // 5 | // Created by Sam Rothenberg on 8/10/14. 6 | // Copyright (c) 2014 Sam Rothenberg. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | extern const NSString *TodayScriptLabelKey; 12 | extern const NSString *TodayScriptProgramKey; 13 | extern const NSString *TodayScriptScriptKey; 14 | extern const NSString *TodayScriptAutoRunKey; 15 | extern const NSString *TodayScriptShowStatusKey; 16 | 17 | // The protocol that this service will vend as its API. This header file will 18 | // also need to be visible to the process hosting the service. Replace the API 19 | // of this protocol with an API appropriate to the service you are vending. 20 | 21 | typedef void (^ XPCHandler )(int status, NSString *output); 22 | 23 | @protocol XPCHelping 24 | 25 | - (void)launchScript:(NSDictionary *)script forUUID:(NSString *)UUID handler:(XPCHandler)handler; 26 | 27 | - (void)terminateScriptForUUID:(NSString *)UUID; 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /TodayScripts.m: -------------------------------------------------------------------------------- 1 | // 2 | // TodayScripts.m 3 | // Today Scripts 4 | // 5 | // Created by Sam Rothenberg on 8/14/14. 6 | // Copyright (c) 2014 Sam Rothenberg. All rights reserved. 7 | // 8 | 9 | #import "TodayScripts.h" 10 | 11 | const NSString *TodayScriptLabelKey = @"Label"; 12 | const NSString *TodayScriptProgramKey = @"Program"; 13 | const NSString *TodayScriptScriptKey = @"Script"; 14 | const NSString *TodayScriptAutoRunKey = @"AutoRun"; 15 | const NSString *TodayScriptShowStatusKey = @"ShowStatus"; 16 | -------------------------------------------------------------------------------- /Widget/AMR_ANSIEscapeHelper/AMR_ANSIEscapeHelper.h: -------------------------------------------------------------------------------- 1 | // 2 | // ANSIEscapeHelper.h 3 | // AnsiColorsTest 4 | // 5 | // Created by Ali Rantakari on 18.3.09. 6 | // 7 | // Version 0.9.6 8 | // 9 | /* 10 | The MIT License 11 | 12 | Copyright (c) 2008-2009,2013 Ali Rantakari 13 | 14 | Permission is hereby granted, free of charge, to any person obtaining a copy 15 | of this software and associated documentation files (the "Software"), to deal 16 | in the Software without restriction, including without limitation the rights 17 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | copies of the Software, and to permit persons to whom the Software is 19 | furnished to do so, subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included in 22 | all copies or substantial portions of the Software. 23 | 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 27 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 29 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 30 | THE SOFTWARE. 31 | */ 32 | 33 | #import 34 | 35 | 36 | #if !__has_feature(objc_arc) 37 | #warning "This code requires ARC to be enabled." 38 | #endif 39 | 40 | 41 | // dictionary keys for the SGR code dictionaries that the array 42 | // escapeCodesForString:cleanString: returns contains 43 | #define kAMRCodeDictKey_code @"code" 44 | #define kAMRCodeDictKey_location @"location" 45 | 46 | // dictionary keys for the string formatting attribute 47 | // dictionaries that the array attributesForString:cleanString: 48 | // returns contains 49 | #define kAMRAttrDictKey_range @"range" 50 | #define kAMRAttrDictKey_attrName @"attributeName" 51 | #define kAMRAttrDictKey_attrValue @"attributeValue" 52 | 53 | 54 | /*! 55 | @enum AMR_SGRCode 56 | 57 | @abstract SGR (Select Graphic Rendition) ANSI control codes. 58 | */ 59 | typedef enum 60 | { 61 | AMR_SGRCodeNoneOrInvalid = -1, 62 | 63 | AMR_SGRCodeAllReset = 0, 64 | 65 | AMR_SGRCodeIntensityBold = 1, 66 | AMR_SGRCodeIntensityFaint = 2, 67 | AMR_SGRCodeIntensityNormal = 22, 68 | 69 | AMR_SGRCodeItalicOn = 3, 70 | 71 | AMR_SGRCodeUnderlineSingle = 4, 72 | AMR_SGRCodeUnderlineDouble = 21, 73 | AMR_SGRCodeUnderlineNone = 24, 74 | 75 | AMR_SGRCodeFgBlack = 30, 76 | AMR_SGRCodeFgRed = 31, 77 | AMR_SGRCodeFgGreen = 32, 78 | AMR_SGRCodeFgYellow = 33, 79 | AMR_SGRCodeFgBlue = 34, 80 | AMR_SGRCodeFgMagenta = 35, 81 | AMR_SGRCodeFgCyan = 36, 82 | AMR_SGRCodeFgWhite = 37, 83 | AMR_SGRCodeFgReset = 39, 84 | 85 | AMR_SGRCodeBgBlack = 40, 86 | AMR_SGRCodeBgRed = 41, 87 | AMR_SGRCodeBgGreen = 42, 88 | AMR_SGRCodeBgYellow = 43, 89 | AMR_SGRCodeBgBlue = 44, 90 | AMR_SGRCodeBgMagenta = 45, 91 | AMR_SGRCodeBgCyan = 46, 92 | AMR_SGRCodeBgWhite = 47, 93 | AMR_SGRCodeBgReset = 49, 94 | 95 | AMR_SGRCodeFgBrightBlack = 90, 96 | AMR_SGRCodeFgBrightRed = 91, 97 | AMR_SGRCodeFgBrightGreen = 92, 98 | AMR_SGRCodeFgBrightYellow = 93, 99 | AMR_SGRCodeFgBrightBlue = 94, 100 | AMR_SGRCodeFgBrightMagenta = 95, 101 | AMR_SGRCodeFgBrightCyan = 96, 102 | AMR_SGRCodeFgBrightWhite = 97, 103 | 104 | AMR_SGRCodeBgBrightBlack = 100, 105 | AMR_SGRCodeBgBrightRed = 101, 106 | AMR_SGRCodeBgBrightGreen = 102, 107 | AMR_SGRCodeBgBrightYellow = 103, 108 | AMR_SGRCodeBgBrightBlue = 104, 109 | AMR_SGRCodeBgBrightMagenta = 105, 110 | AMR_SGRCodeBgBrightCyan = 106, 111 | AMR_SGRCodeBgBrightWhite = 107 112 | } AMR_SGRCode; 113 | 114 | 115 | 116 | 117 | 118 | 119 | /*! 120 | @class AMR_ANSIEscapeHelper 121 | 122 | @abstract Contains helper methods for dealing with strings 123 | that contain ANSI escape sequences for formatting (colors, 124 | underlining, bold etc.) 125 | */ 126 | @interface AMR_ANSIEscapeHelper : NSObject 127 | 128 | /*! 129 | @property defaultStringColor 130 | 131 | @abstract The default color used when creating an attributed string (default is black). 132 | */ 133 | @property(copy) NSColor *defaultStringColor; 134 | 135 | 136 | /*! 137 | @property font 138 | 139 | @abstract The font to use when creating string formatting attribute values. 140 | */ 141 | @property(copy) NSFont *font; 142 | 143 | /*! 144 | @property ansiColors 145 | 146 | @abstract The colors to use for displaying ANSI colors. 147 | 148 | @discussion Keys in this dictionary should be NSNumber objects containing SGR code 149 | values from the AMR_SGRCode enum. The corresponding values for these keys 150 | should be NSColor objects. If this property is nil or if it doesn't 151 | contain a key for a specific SGR code, the default color will be used 152 | instead. 153 | */ 154 | @property(retain) NSMutableDictionary *ansiColors; 155 | 156 | 157 | /*! 158 | @method attributedStringWithANSIEscapedString: 159 | 160 | @abstract Returns an attributed string that corresponds both in contents 161 | and formatting to a given string that contains ANSI escape 162 | sequences. 163 | 164 | @param aString A String containing ANSI escape sequences 165 | 166 | @result An attributed string that mimics as closely as possible 167 | the formatting of the given ANSI-escaped string. 168 | */ 169 | - (NSAttributedString*) attributedStringWithANSIEscapedString:(NSString*)aString; 170 | 171 | 172 | /*! 173 | @method ansiEscapedStringWithAttributedString: 174 | 175 | @abstract Returns a string containing ANSI escape sequences that corresponds 176 | both in contents and formatting to a given attributed string. 177 | 178 | @param aAttributedString An attributed string 179 | 180 | @result A string that mimics as closely as possible 181 | the formatting of the given attributed string with 182 | ANSI escape sequences. 183 | */ 184 | - (NSString*) ansiEscapedStringWithAttributedString:(NSAttributedString*)aAttributedString; 185 | 186 | 187 | /*! 188 | @method escapeCodesForString:cleanString: 189 | 190 | @abstract Returns an array of SGR codes and their locations from a 191 | string containing ANSI escape sequences as well as a "clean" 192 | version of the string (i.e. one without the ANSI escape 193 | sequences.) 194 | 195 | @param aString A String containing ANSI escape sequences 196 | @param aCleanString Upon return, contains a "clean" version of aString (i.e. aString 197 | without the ANSI escape sequences) 198 | 199 | @result An array of NSDictionary objects, each of which has 200 | an NSNumber value for the key "code" (specifying an SGR code) and 201 | another NSNumber value for the key "location" (specifying the 202 | location of the code within aCleanString.) 203 | */ 204 | - (NSArray*) escapeCodesForString:(NSString*)aString cleanString:(NSString**)aCleanString; 205 | 206 | 207 | /*! 208 | @method ansiEscapedStringWithCodesAndLocations:cleanString: 209 | 210 | @abstract Returns a string containing ANSI escape codes for formatting based 211 | on a string and an array of SGR codes and their locations within 212 | the given string. 213 | 214 | @param aCodesArray An array of NSDictionary objects, each of which should have 215 | an NSNumber value for the key "code" (specifying an SGR 216 | code) and another NSNumber value for the key "location" 217 | (specifying the location of this SGR code in aCleanString.) 218 | @param aCleanString The string to which to insert the ANSI escape codes 219 | described in aCodesArray. 220 | 221 | @result A string containing ANSI escape sequences. 222 | */ 223 | - (NSString*) ansiEscapedStringWithCodesAndLocations:(NSArray*)aCodesArray cleanString:(NSString*)aCleanString; 224 | 225 | 226 | /*! 227 | @method attributesForString:cleanString: 228 | 229 | @abstract Convert ANSI escape sequences in a string to string formatting attributes. 230 | 231 | @discussion Given a string with some ANSI escape sequences in it, this method returns 232 | attributes for formatting the specified string according to those ANSI 233 | escape sequences as well as a "clean" (i.e. free of the escape sequences) 234 | version of this string. 235 | 236 | @param aString A String containing ANSI escape sequences 237 | @param aCleanString Upon return, contains a "clean" version of aString (i.e. aString 238 | without the ANSI escape sequences.) Pass in NULL if you're not 239 | interested in this. 240 | 241 | @result An array containing NSDictionary objects, each of which has keys "range" 242 | (an NSValue containing an NSRange, specifying the range for the 243 | attribute within the "clean" version of aString), "attributeName" (an 244 | NSString) and "attributeValue" (an NSObject). You may use these as 245 | arguments for NSMutableAttributedString's methods for setting the 246 | visual formatting. 247 | */ 248 | - (NSArray*) attributesForString:(NSString*)aString cleanString:(NSString**)aCleanString; 249 | 250 | 251 | /*! 252 | @method AMR_SGRCode:endsFormattingIntroducedByCode: 253 | 254 | @abstract Whether the occurrence of a given SGR code would end the formatting run 255 | introduced by another SGR code. 256 | 257 | @discussion For example, AMR_SGRCodeFgReset, AMR_SGRCodeAllReset or any SGR code 258 | specifying a foreground color would end the formatting run 259 | introduced by a foreground color -specifying SGR code. 260 | 261 | @param endCode The SGR code to test as a candidate for ending the formatting run 262 | introduced by startCode 263 | @param startCode The SGR code that has introduced a formatting run 264 | 265 | @result YES if the occurrence of endCode would end the formatting run 266 | introduced by startCode, NO otherwise. 267 | */ 268 | - (BOOL) AMR_SGRCode:(AMR_SGRCode)endCode endsFormattingIntroducedByCode:(AMR_SGRCode)startCode; 269 | 270 | 271 | /*! 272 | @method colorForSGRCode: 273 | 274 | @abstract Returns the color to use for displaying a specific ANSI color. 275 | 276 | @discussion This method first considers the values set in the ansiColors 277 | property and only then the standard basic colors (NSColor's 278 | redColor, blueColor etc.) 279 | 280 | @param code An SGR code that specifies an ANSI color. 281 | 282 | @result The color to use for displaying the ANSI color specified by code. 283 | */ 284 | - (NSColor*) colorForSGRCode:(AMR_SGRCode)code; 285 | 286 | 287 | /*! 288 | @method AMR_SGRCodeForColor:isForegroundColor: 289 | 290 | @abstract Returns a color SGR code that corresponds to a given color. 291 | 292 | @discussion This method matches colors to their equivalent SGR codes 293 | by going through the colors specified in the ansiColors 294 | dictionary, and if ansiColors is null or if a match is 295 | not found there, by comparing the given color to the 296 | standard basic colors (NSColor's redColor, blueColor 297 | etc.) The comparison is done simply by checking for 298 | equality. 299 | 300 | @param aColor The color to get a corresponding SGR code for 301 | @param aForeground Whether you want a foreground or background color code 302 | 303 | @result SGR code that corresponds with aColor. 304 | */ 305 | - (AMR_SGRCode) AMR_SGRCodeForColor:(NSColor*)aColor isForegroundColor:(BOOL)aForeground; 306 | 307 | 308 | /*! 309 | @method closestSGRCodeForColor:isForegroundColor: 310 | 311 | @abstract Returns a color SGR code that represents the closest ANSI 312 | color to a given color. 313 | 314 | @discussion This method attempts to find the closest ANSI color to 315 | aColor and return its SGR code. 316 | 317 | @param aColor The color to get a closest color SGR code match for 318 | @param aForeground Whether you want a foreground or background color code 319 | 320 | @result SGR code for the ANSI color that is closest to aColor. 321 | */ 322 | - (AMR_SGRCode) closestSGRCodeForColor:(NSColor *)color isForegroundColor:(BOOL)foreground; 323 | 324 | 325 | 326 | @end 327 | -------------------------------------------------------------------------------- /Widget/AMR_ANSIEscapeHelper/AMR_ANSIEscapeHelper.m: -------------------------------------------------------------------------------- 1 | // 2 | // ANSIEscapeHelper.m 3 | // 4 | // Created by Ali Rantakari on 18.3.09. 5 | 6 | /* 7 | The MIT License 8 | 9 | Copyright (c) 2008-2009,2013 Ali Rantakari 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in 19 | all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | THE SOFTWARE. 28 | */ 29 | 30 | /* 31 | todo: 32 | 33 | - don't add useless "reset" escape codes to the string in 34 | -ansiEscapedStringWithAttributedString: 35 | 36 | */ 37 | 38 | 39 | 40 | #import "AMR_ANSIEscapeHelper.h" 41 | 42 | 43 | // the CSI (Control Sequence Initiator) -- i.e. "escape sequence prefix". 44 | // (add your own CSI:Miami joke here) 45 | #define kANSIEscapeCSI @"\033[" 46 | 47 | // the end byte of an SGR (Select Graphic Rendition) 48 | // ANSI Escape Sequence 49 | #define kANSIEscapeSGREnd @"m" 50 | 51 | 52 | // color definition helper macros 53 | #define kBrightColorBrightness 1.0 54 | #define kBrightColorSaturation 0.4 55 | #define kBrightColorAlpha 1.0 56 | #define kBrightColorWithHue(h) [NSColor colorWithCalibratedHue:(h) saturation:kBrightColorSaturation brightness:kBrightColorBrightness alpha:kBrightColorAlpha] 57 | 58 | // default colors 59 | #define kDefaultANSIColorFgBlack NSColor.blackColor 60 | #define kDefaultANSIColorFgRed NSColor.redColor 61 | #define kDefaultANSIColorFgGreen NSColor.greenColor 62 | #define kDefaultANSIColorFgYellow NSColor.yellowColor 63 | #define kDefaultANSIColorFgBlue NSColor.blueColor 64 | #define kDefaultANSIColorFgMagenta NSColor.magentaColor 65 | #define kDefaultANSIColorFgCyan NSColor.cyanColor 66 | #define kDefaultANSIColorFgWhite NSColor.whiteColor 67 | 68 | #define kDefaultANSIColorFgBrightBlack [NSColor colorWithCalibratedWhite:0.337 alpha:1.0] 69 | #define kDefaultANSIColorFgBrightRed kBrightColorWithHue(1.0) 70 | #define kDefaultANSIColorFgBrightGreen kBrightColorWithHue(1.0/3.0) 71 | #define kDefaultANSIColorFgBrightYellow kBrightColorWithHue(1.0/6.0) 72 | #define kDefaultANSIColorFgBrightBlue kBrightColorWithHue(2.0/3.0) 73 | #define kDefaultANSIColorFgBrightMagenta kBrightColorWithHue(5.0/6.0) 74 | #define kDefaultANSIColorFgBrightCyan kBrightColorWithHue(0.5) 75 | #define kDefaultANSIColorFgBrightWhite NSColor.whiteColor 76 | 77 | #define kDefaultANSIColorBgBlack NSColor.blackColor 78 | #define kDefaultANSIColorBgRed NSColor.redColor 79 | #define kDefaultANSIColorBgGreen NSColor.greenColor 80 | #define kDefaultANSIColorBgYellow NSColor.yellowColor 81 | #define kDefaultANSIColorBgBlue NSColor.blueColor 82 | #define kDefaultANSIColorBgMagenta NSColor.magentaColor 83 | #define kDefaultANSIColorBgCyan NSColor.cyanColor 84 | #define kDefaultANSIColorBgWhite NSColor.whiteColor 85 | 86 | #define kDefaultANSIColorBgBrightBlack kDefaultANSIColorFgBrightBlack 87 | #define kDefaultANSIColorBgBrightRed kDefaultANSIColorFgBrightRed 88 | #define kDefaultANSIColorBgBrightGreen kDefaultANSIColorFgBrightGreen 89 | #define kDefaultANSIColorBgBrightYellow kDefaultANSIColorFgBrightYellow 90 | #define kDefaultANSIColorBgBrightBlue kDefaultANSIColorFgBrightBlue 91 | #define kDefaultANSIColorBgBrightMagenta kDefaultANSIColorFgBrightMagenta 92 | #define kDefaultANSIColorBgBrightCyan kDefaultANSIColorFgBrightCyan 93 | #define kDefaultANSIColorBgBrightWhite kDefaultANSIColorFgBrightWhite 94 | 95 | #define kDefaultFontSize [NSFont systemFontOfSize:NSFont.systemFontSize] 96 | #define kDefaultForegroundColor NSColor.blackColor 97 | 98 | // minimum weight for an NSFont for it to be considered bold 99 | #define kBoldFontMinWeight 9 100 | 101 | 102 | @implementation AMR_ANSIEscapeHelper 103 | 104 | - (id) init 105 | { 106 | if (!(self = [super init])) 107 | return nil; 108 | 109 | self.ansiColors = [NSMutableDictionary dictionary]; 110 | 111 | return self; 112 | } 113 | 114 | 115 | 116 | - (NSAttributedString*) attributedStringWithANSIEscapedString:(NSString*)aString 117 | { 118 | if (aString == nil) 119 | return nil; 120 | 121 | NSString *cleanString; 122 | NSArray *attributesAndRanges = [self attributesForString:aString cleanString:&cleanString]; 123 | NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] 124 | initWithString:cleanString 125 | attributes:@{ 126 | NSFontAttributeName: self.font ?: kDefaultFontSize, 127 | NSForegroundColorAttributeName: self.defaultStringColor ?: kDefaultForegroundColor 128 | }]; 129 | 130 | for (NSDictionary *thisAttributeDict in attributesAndRanges) 131 | { 132 | [attributedString 133 | addAttribute:thisAttributeDict[kAMRAttrDictKey_attrName] 134 | value:thisAttributeDict[kAMRAttrDictKey_attrValue] 135 | range:[thisAttributeDict[kAMRAttrDictKey_range] rangeValue] 136 | ]; 137 | } 138 | 139 | return attributedString; 140 | } 141 | 142 | 143 | 144 | - (NSString*) ansiEscapedStringWithAttributedString:(NSAttributedString*)aAttributedString 145 | { 146 | NSMutableArray *codesAndLocations = [NSMutableArray array]; 147 | 148 | NSArray *attrNames = @[ 149 | NSFontAttributeName, NSForegroundColorAttributeName, 150 | NSBackgroundColorAttributeName, NSUnderlineStyleAttributeName, 151 | ]; 152 | 153 | for (NSString *thisAttrName in attrNames) 154 | { 155 | NSRange limitRange = NSMakeRange(0, aAttributedString.length); 156 | id attributeValue; 157 | NSRange effectiveRange; 158 | 159 | while (limitRange.length > 0) 160 | { 161 | attributeValue = [aAttributedString 162 | attribute:thisAttrName 163 | atIndex:limitRange.location 164 | longestEffectiveRange:&effectiveRange 165 | inRange:limitRange 166 | ]; 167 | 168 | AMR_SGRCode thisSGRCode = AMR_SGRCodeNoneOrInvalid; 169 | 170 | if ([thisAttrName isEqualToString:NSForegroundColorAttributeName]) 171 | { 172 | if (attributeValue != nil) 173 | thisSGRCode = [self closestSGRCodeForColor:attributeValue isForegroundColor:YES]; 174 | else 175 | thisSGRCode = AMR_SGRCodeFgReset; 176 | } 177 | else if ([thisAttrName isEqualToString:NSBackgroundColorAttributeName]) 178 | { 179 | if (attributeValue != nil) 180 | thisSGRCode = [self closestSGRCodeForColor:attributeValue isForegroundColor:NO]; 181 | else 182 | thisSGRCode = AMR_SGRCodeBgReset; 183 | } 184 | else if ([thisAttrName isEqualToString:NSFontAttributeName]) 185 | { 186 | // we currently only use NSFontAttributeName for bolding so 187 | // here we assume that the formatting "type" in ANSI SGR 188 | // terms is indeed intensity 189 | if (attributeValue != nil) 190 | thisSGRCode = ([NSFontManager.sharedFontManager weightOfFont:attributeValue] >= kBoldFontMinWeight) 191 | ? AMR_SGRCodeIntensityBold : AMR_SGRCodeIntensityNormal; 192 | else 193 | thisSGRCode = AMR_SGRCodeIntensityNormal; 194 | } 195 | else if ([thisAttrName isEqualToString:NSUnderlineStyleAttributeName]) 196 | { 197 | if (attributeValue != nil) 198 | { 199 | if ([attributeValue intValue] == NSUnderlineStyleSingle) 200 | thisSGRCode = AMR_SGRCodeUnderlineSingle; 201 | else if ([attributeValue intValue] == NSUnderlineStyleDouble) 202 | thisSGRCode = AMR_SGRCodeUnderlineDouble; 203 | else 204 | thisSGRCode = AMR_SGRCodeUnderlineNone; 205 | } 206 | else 207 | thisSGRCode = AMR_SGRCodeUnderlineNone; 208 | } 209 | 210 | if (thisSGRCode != AMR_SGRCodeNoneOrInvalid) 211 | { 212 | [codesAndLocations addObject: @{ 213 | kAMRCodeDictKey_code: @(thisSGRCode), 214 | kAMRCodeDictKey_location: @(effectiveRange.location), 215 | }]; 216 | } 217 | 218 | limitRange = NSMakeRange(NSMaxRange(effectiveRange), 219 | NSMaxRange(limitRange) - NSMaxRange(effectiveRange)); 220 | } 221 | } 222 | 223 | return [self ansiEscapedStringWithCodesAndLocations:codesAndLocations cleanString:aAttributedString.string]; 224 | } 225 | 226 | 227 | - (NSArray*) escapeCodesForString:(NSString*)aString cleanString:(NSString**)aCleanString 228 | { 229 | if (aString == nil) 230 | return nil; 231 | if (aString.length <= kANSIEscapeCSI.length) 232 | { 233 | if (aCleanString) 234 | *aCleanString = aString.copy; 235 | return @[]; 236 | } 237 | 238 | NSString *cleanString = @""; 239 | 240 | // find all escape sequence codes from aString and put them in this array 241 | // along with their start locations within the "clean" version of aString 242 | NSMutableArray *formatCodes = [NSMutableArray array]; 243 | 244 | NSUInteger aStringLength = aString.length; 245 | NSUInteger coveredLength = 0; 246 | NSRange searchRange = NSMakeRange(0,aStringLength); 247 | NSRange thisEscapeSequenceRange; 248 | do 249 | { 250 | thisEscapeSequenceRange = [aString rangeOfString:kANSIEscapeCSI options:NSLiteralSearch range:searchRange]; 251 | if (thisEscapeSequenceRange.location != NSNotFound) 252 | { 253 | // adjust range's length so that it encompasses the whole ANSI escape sequence 254 | // and not just the Control Sequence Initiator (the "prefix") by finding the 255 | // final byte of the control sequence (one that has an ASCII decimal value 256 | // between 64 and 126.) at the same time, read all formatting codes from inside 257 | // this escape sequence (there may be several, separated by semicolons.) 258 | NSMutableArray *codes = [NSMutableArray array]; 259 | unsigned int code = 0; 260 | unsigned int lengthAddition = 1; 261 | NSUInteger thisIndex; 262 | for (;;) 263 | { 264 | thisIndex = (NSMaxRange(thisEscapeSequenceRange)+lengthAddition-1); 265 | if (thisIndex >= aStringLength) 266 | break; 267 | 268 | unichar c = [aString characterAtIndex:thisIndex]; 269 | 270 | if (('0' <= c) && (c <= '9')) 271 | { 272 | int digit = c - '0'; 273 | code = (code == 0) ? digit : code*10+digit; 274 | } 275 | 276 | // ASCII decimal 109 is the SGR (Select Graphic Rendition) final byte 277 | // ("m"). this means that the code value we've just read specifies formatting 278 | // for the output; exactly what we're interested in. 279 | if (c == 'm') 280 | { 281 | [codes addObject:@(code)]; 282 | break; 283 | } 284 | else if ((64 <= c) && (c <= 126)) // any other valid final byte 285 | { 286 | [codes removeAllObjects]; 287 | break; 288 | } 289 | else if (c == ';') // separates codes within the same sequence 290 | { 291 | [codes addObject:@(code)]; 292 | code = 0; 293 | } 294 | 295 | lengthAddition++; 296 | } 297 | thisEscapeSequenceRange.length += lengthAddition; 298 | 299 | NSUInteger locationInCleanString = coveredLength+thisEscapeSequenceRange.location-searchRange.location; 300 | 301 | for (NSNumber *codeToAdd in codes) 302 | { 303 | [formatCodes addObject: @{ 304 | kAMRCodeDictKey_code: codeToAdd, 305 | kAMRCodeDictKey_location: @(locationInCleanString) 306 | }]; 307 | } 308 | 309 | NSUInteger thisCoveredLength = thisEscapeSequenceRange.location-searchRange.location; 310 | if (thisCoveredLength > 0) 311 | cleanString = [cleanString stringByAppendingString:[aString substringWithRange:NSMakeRange(searchRange.location, thisCoveredLength)]]; 312 | 313 | coveredLength += thisCoveredLength; 314 | searchRange.location = NSMaxRange(thisEscapeSequenceRange); 315 | searchRange.length = aStringLength-searchRange.location; 316 | } 317 | } 318 | while(thisEscapeSequenceRange.location != NSNotFound); 319 | 320 | if (searchRange.length > 0) 321 | cleanString = [cleanString stringByAppendingString:[aString substringWithRange:searchRange]]; 322 | 323 | if (aCleanString) 324 | *aCleanString = cleanString; 325 | return formatCodes; 326 | } 327 | 328 | 329 | 330 | 331 | - (NSString*) ansiEscapedStringWithCodesAndLocations:(NSArray*)aCodesArray cleanString:(NSString*)aCleanString 332 | { 333 | NSMutableString* retStr = [NSMutableString stringWithCapacity:aCleanString.length]; 334 | 335 | NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:kAMRCodeDictKey_location ascending:YES]; 336 | NSArray *codesArray = [aCodesArray sortedArrayUsingDescriptors:@[sortDescriptor]]; 337 | 338 | NSUInteger aCleanStringIndex = 0; 339 | NSUInteger aCleanStringLength = aCleanString.length; 340 | for (NSDictionary *thisCodeDict in codesArray) 341 | { 342 | if (!( thisCodeDict[kAMRCodeDictKey_code] && 343 | thisCodeDict[kAMRCodeDictKey_location] 344 | )) 345 | continue; 346 | 347 | AMR_SGRCode thisCode = [thisCodeDict[kAMRCodeDictKey_code] unsignedIntValue]; 348 | NSUInteger formattingRunStartLocation = [thisCodeDict[kAMRCodeDictKey_location] unsignedIntegerValue]; 349 | 350 | if (formattingRunStartLocation > aCleanStringLength) 351 | continue; 352 | 353 | if (aCleanStringIndex < formattingRunStartLocation) 354 | [retStr appendString:[aCleanString substringWithRange:NSMakeRange(aCleanStringIndex, formattingRunStartLocation-aCleanStringIndex)]]; 355 | [retStr appendFormat:@"%@%d%@", kANSIEscapeCSI, thisCode, kANSIEscapeSGREnd]; 356 | 357 | aCleanStringIndex = formattingRunStartLocation; 358 | } 359 | 360 | if (aCleanStringIndex < aCleanStringLength) 361 | [retStr appendString:[aCleanString substringFromIndex:aCleanStringIndex]]; 362 | 363 | [retStr appendFormat:@"%@%d%@", kANSIEscapeCSI, AMR_SGRCodeAllReset, kANSIEscapeSGREnd]; 364 | 365 | return retStr; 366 | } 367 | 368 | 369 | 370 | 371 | 372 | - (NSArray*) attributesForString:(NSString*)aString cleanString:(NSString**)aCleanString 373 | { 374 | if (aString == nil) 375 | return nil; 376 | if (aString.length <= kANSIEscapeCSI.length) 377 | { 378 | if (aCleanString) 379 | *aCleanString = aString.copy; 380 | return @[]; 381 | } 382 | 383 | NSMutableArray *attrsAndRanges = [NSMutableArray array]; 384 | 385 | NSString *cleanString; 386 | NSArray *formatCodes = [self escapeCodesForString:aString cleanString:&cleanString]; 387 | 388 | // go through all the found escape sequence codes and for each one, create 389 | // the string formatting attribute name and value, find the next escape 390 | // sequence that specifies the end of the formatting run started by 391 | // the currently handled code, and generate a range from the difference 392 | // in those codes' locations within the clean aString. 393 | for (NSUInteger iCode = 0; iCode < formatCodes.count; iCode++) 394 | { 395 | NSDictionary *thisCodeDict = formatCodes[iCode]; 396 | AMR_SGRCode thisCode = [thisCodeDict[kAMRCodeDictKey_code] unsignedIntValue]; 397 | NSUInteger formattingRunStartLocation = [thisCodeDict[kAMRCodeDictKey_location] unsignedIntegerValue]; 398 | 399 | // the attributed string attribute name for the formatting run introduced 400 | // by this code 401 | NSString *thisAttributeName = nil; 402 | 403 | // the attributed string attribute value for this formatting run introduced 404 | // by this code 405 | NSObject *thisAttributeValue = nil; 406 | 407 | // set attribute name 408 | switch(thisCode) 409 | { 410 | case AMR_SGRCodeFgBlack: 411 | case AMR_SGRCodeFgRed: 412 | case AMR_SGRCodeFgGreen: 413 | case AMR_SGRCodeFgYellow: 414 | case AMR_SGRCodeFgBlue: 415 | case AMR_SGRCodeFgMagenta: 416 | case AMR_SGRCodeFgCyan: 417 | case AMR_SGRCodeFgWhite: 418 | case AMR_SGRCodeFgBrightBlack: 419 | case AMR_SGRCodeFgBrightRed: 420 | case AMR_SGRCodeFgBrightGreen: 421 | case AMR_SGRCodeFgBrightYellow: 422 | case AMR_SGRCodeFgBrightBlue: 423 | case AMR_SGRCodeFgBrightMagenta: 424 | case AMR_SGRCodeFgBrightCyan: 425 | case AMR_SGRCodeFgBrightWhite: 426 | thisAttributeName = NSForegroundColorAttributeName; 427 | break; 428 | case AMR_SGRCodeBgBlack: 429 | case AMR_SGRCodeBgRed: 430 | case AMR_SGRCodeBgGreen: 431 | case AMR_SGRCodeBgYellow: 432 | case AMR_SGRCodeBgBlue: 433 | case AMR_SGRCodeBgMagenta: 434 | case AMR_SGRCodeBgCyan: 435 | case AMR_SGRCodeBgWhite: 436 | case AMR_SGRCodeBgBrightBlack: 437 | case AMR_SGRCodeBgBrightRed: 438 | case AMR_SGRCodeBgBrightGreen: 439 | case AMR_SGRCodeBgBrightYellow: 440 | case AMR_SGRCodeBgBrightBlue: 441 | case AMR_SGRCodeBgBrightMagenta: 442 | case AMR_SGRCodeBgBrightCyan: 443 | case AMR_SGRCodeBgBrightWhite: 444 | thisAttributeName = NSBackgroundColorAttributeName; 445 | break; 446 | case AMR_SGRCodeIntensityBold: 447 | case AMR_SGRCodeIntensityNormal: 448 | case AMR_SGRCodeIntensityFaint: 449 | thisAttributeName = NSFontAttributeName; 450 | break; 451 | case AMR_SGRCodeUnderlineSingle: 452 | case AMR_SGRCodeUnderlineDouble: 453 | case AMR_SGRCodeUnderlineNone: 454 | thisAttributeName = NSUnderlineStyleAttributeName; 455 | break; 456 | case AMR_SGRCodeAllReset: 457 | case AMR_SGRCodeFgReset: 458 | case AMR_SGRCodeBgReset: 459 | case AMR_SGRCodeNoneOrInvalid: 460 | case AMR_SGRCodeItalicOn: 461 | continue; 462 | } 463 | 464 | // set attribute value 465 | switch(thisCode) 466 | { 467 | case AMR_SGRCodeBgBlack: 468 | case AMR_SGRCodeFgBlack: 469 | case AMR_SGRCodeBgRed: 470 | case AMR_SGRCodeFgRed: 471 | case AMR_SGRCodeBgGreen: 472 | case AMR_SGRCodeFgGreen: 473 | case AMR_SGRCodeBgYellow: 474 | case AMR_SGRCodeFgYellow: 475 | case AMR_SGRCodeBgBlue: 476 | case AMR_SGRCodeFgBlue: 477 | case AMR_SGRCodeBgMagenta: 478 | case AMR_SGRCodeFgMagenta: 479 | case AMR_SGRCodeBgCyan: 480 | case AMR_SGRCodeFgCyan: 481 | case AMR_SGRCodeBgWhite: 482 | case AMR_SGRCodeFgWhite: 483 | case AMR_SGRCodeBgBrightBlack: 484 | case AMR_SGRCodeFgBrightBlack: 485 | case AMR_SGRCodeBgBrightRed: 486 | case AMR_SGRCodeFgBrightRed: 487 | case AMR_SGRCodeBgBrightGreen: 488 | case AMR_SGRCodeFgBrightGreen: 489 | case AMR_SGRCodeBgBrightYellow: 490 | case AMR_SGRCodeFgBrightYellow: 491 | case AMR_SGRCodeBgBrightBlue: 492 | case AMR_SGRCodeFgBrightBlue: 493 | case AMR_SGRCodeBgBrightMagenta: 494 | case AMR_SGRCodeFgBrightMagenta: 495 | case AMR_SGRCodeBgBrightCyan: 496 | case AMR_SGRCodeFgBrightCyan: 497 | case AMR_SGRCodeBgBrightWhite: 498 | case AMR_SGRCodeFgBrightWhite: 499 | thisAttributeValue = [self colorForSGRCode:thisCode]; 500 | break; 501 | case AMR_SGRCodeIntensityBold: 502 | { 503 | NSFont *boldFont = [NSFontManager.sharedFontManager convertFont:self.font toHaveTrait:NSBoldFontMask]; 504 | thisAttributeValue = boldFont; 505 | } 506 | break; 507 | case AMR_SGRCodeIntensityNormal: 508 | case AMR_SGRCodeIntensityFaint: 509 | { 510 | NSFont *unboldFont = [NSFontManager.sharedFontManager convertFont:self.font toHaveTrait:NSUnboldFontMask]; 511 | thisAttributeValue = unboldFont; 512 | } 513 | break; 514 | case AMR_SGRCodeUnderlineSingle: 515 | thisAttributeValue = @(NSUnderlineStyleSingle); 516 | break; 517 | case AMR_SGRCodeUnderlineDouble: 518 | thisAttributeValue = @(NSUnderlineStyleDouble); 519 | break; 520 | case AMR_SGRCodeUnderlineNone: 521 | thisAttributeValue = @(NSUnderlineStyleNone); 522 | break; 523 | case AMR_SGRCodeAllReset: 524 | case AMR_SGRCodeFgReset: 525 | case AMR_SGRCodeBgReset: 526 | case AMR_SGRCodeNoneOrInvalid: 527 | case AMR_SGRCodeItalicOn: 528 | break; 529 | } 530 | 531 | 532 | // find the next sequence that specifies the end of this formatting run 533 | NSInteger formattingRunEndLocation = -1; 534 | if (iCode < (formatCodes.count - 1)) 535 | { 536 | NSDictionary *thisEndCodeCandidateDict; 537 | unichar thisEndCodeCandidate; 538 | for (NSUInteger iEndCode = iCode+1; iEndCode < formatCodes.count; iEndCode++) 539 | { 540 | thisEndCodeCandidateDict = formatCodes[iEndCode]; 541 | thisEndCodeCandidate = [thisEndCodeCandidateDict[kAMRCodeDictKey_code] unsignedIntValue]; 542 | 543 | if ([self AMR_SGRCode:thisEndCodeCandidate endsFormattingIntroducedByCode:thisCode]) 544 | { 545 | formattingRunEndLocation = [thisEndCodeCandidateDict[kAMRCodeDictKey_location] unsignedIntegerValue]; 546 | break; 547 | } 548 | } 549 | } 550 | if (formattingRunEndLocation == -1) 551 | formattingRunEndLocation = cleanString.length; 552 | 553 | if (thisAttributeName && thisAttributeValue) 554 | { 555 | [attrsAndRanges addObject:@{ 556 | kAMRAttrDictKey_range: [NSValue valueWithRange:NSMakeRange(formattingRunStartLocation, (formattingRunEndLocation-formattingRunStartLocation))], 557 | kAMRAttrDictKey_attrName: thisAttributeName, 558 | kAMRAttrDictKey_attrValue: thisAttributeValue, 559 | }]; 560 | } 561 | } 562 | 563 | if (aCleanString) 564 | *aCleanString = cleanString; 565 | return attrsAndRanges; 566 | } 567 | 568 | 569 | 570 | 571 | 572 | - (BOOL) AMR_SGRCode:(AMR_SGRCode)endCode endsFormattingIntroducedByCode:(AMR_SGRCode)startCode 573 | { 574 | switch(startCode) 575 | { 576 | case AMR_SGRCodeFgBlack: 577 | case AMR_SGRCodeFgRed: 578 | case AMR_SGRCodeFgGreen: 579 | case AMR_SGRCodeFgYellow: 580 | case AMR_SGRCodeFgBlue: 581 | case AMR_SGRCodeFgMagenta: 582 | case AMR_SGRCodeFgCyan: 583 | case AMR_SGRCodeFgWhite: 584 | case AMR_SGRCodeFgBrightBlack: 585 | case AMR_SGRCodeFgBrightRed: 586 | case AMR_SGRCodeFgBrightGreen: 587 | case AMR_SGRCodeFgBrightYellow: 588 | case AMR_SGRCodeFgBrightBlue: 589 | case AMR_SGRCodeFgBrightMagenta: 590 | case AMR_SGRCodeFgBrightCyan: 591 | case AMR_SGRCodeFgBrightWhite: 592 | return (endCode == AMR_SGRCodeAllReset || endCode == AMR_SGRCodeFgReset || 593 | endCode == AMR_SGRCodeFgBlack || endCode == AMR_SGRCodeFgRed || 594 | endCode == AMR_SGRCodeFgGreen || endCode == AMR_SGRCodeFgYellow || 595 | endCode == AMR_SGRCodeFgBlue || endCode == AMR_SGRCodeFgMagenta || 596 | endCode == AMR_SGRCodeFgCyan || endCode == AMR_SGRCodeFgWhite || 597 | endCode == AMR_SGRCodeFgBrightBlack || endCode == AMR_SGRCodeFgBrightRed || 598 | endCode == AMR_SGRCodeFgBrightGreen || endCode == AMR_SGRCodeFgBrightYellow || 599 | endCode == AMR_SGRCodeFgBrightBlue || endCode == AMR_SGRCodeFgBrightMagenta || 600 | endCode == AMR_SGRCodeFgBrightCyan || endCode == AMR_SGRCodeFgBrightWhite); 601 | case AMR_SGRCodeBgBlack: 602 | case AMR_SGRCodeBgRed: 603 | case AMR_SGRCodeBgGreen: 604 | case AMR_SGRCodeBgYellow: 605 | case AMR_SGRCodeBgBlue: 606 | case AMR_SGRCodeBgMagenta: 607 | case AMR_SGRCodeBgCyan: 608 | case AMR_SGRCodeBgWhite: 609 | case AMR_SGRCodeBgBrightBlack: 610 | case AMR_SGRCodeBgBrightRed: 611 | case AMR_SGRCodeBgBrightGreen: 612 | case AMR_SGRCodeBgBrightYellow: 613 | case AMR_SGRCodeBgBrightBlue: 614 | case AMR_SGRCodeBgBrightMagenta: 615 | case AMR_SGRCodeBgBrightCyan: 616 | case AMR_SGRCodeBgBrightWhite: 617 | return (endCode == AMR_SGRCodeAllReset || endCode == AMR_SGRCodeBgReset || 618 | endCode == AMR_SGRCodeBgBlack || endCode == AMR_SGRCodeBgRed || 619 | endCode == AMR_SGRCodeBgGreen || endCode == AMR_SGRCodeBgYellow || 620 | endCode == AMR_SGRCodeBgBlue || endCode == AMR_SGRCodeBgMagenta || 621 | endCode == AMR_SGRCodeBgCyan || endCode == AMR_SGRCodeBgWhite || 622 | endCode == AMR_SGRCodeBgBrightBlack || endCode == AMR_SGRCodeBgBrightRed || 623 | endCode == AMR_SGRCodeBgBrightGreen || endCode == AMR_SGRCodeBgBrightYellow || 624 | endCode == AMR_SGRCodeBgBrightBlue || endCode == AMR_SGRCodeBgBrightMagenta || 625 | endCode == AMR_SGRCodeBgBrightCyan || endCode == AMR_SGRCodeBgBrightWhite); 626 | case AMR_SGRCodeIntensityBold: 627 | case AMR_SGRCodeIntensityNormal: 628 | return (endCode == AMR_SGRCodeAllReset || endCode == AMR_SGRCodeIntensityNormal || 629 | endCode == AMR_SGRCodeIntensityBold || endCode == AMR_SGRCodeIntensityFaint); 630 | case AMR_SGRCodeUnderlineSingle: 631 | case AMR_SGRCodeUnderlineDouble: 632 | return (endCode == AMR_SGRCodeAllReset || endCode == AMR_SGRCodeUnderlineNone || 633 | endCode == AMR_SGRCodeUnderlineSingle || endCode == AMR_SGRCodeUnderlineDouble); 634 | case AMR_SGRCodeNoneOrInvalid: 635 | case AMR_SGRCodeItalicOn: 636 | case AMR_SGRCodeUnderlineNone: 637 | case AMR_SGRCodeIntensityFaint: 638 | case AMR_SGRCodeAllReset: 639 | case AMR_SGRCodeBgReset: 640 | case AMR_SGRCodeFgReset: 641 | return NO; 642 | } 643 | 644 | return NO; 645 | } 646 | 647 | 648 | 649 | 650 | - (NSColor*) colorForSGRCode:(AMR_SGRCode)code 651 | { 652 | if (self.ansiColors) 653 | { 654 | NSColor *preferredColor = self.ansiColors[@(code)]; 655 | if (preferredColor) 656 | return preferredColor; 657 | } 658 | 659 | switch(code) 660 | { 661 | case AMR_SGRCodeFgBlack: 662 | return kDefaultANSIColorFgBlack; 663 | case AMR_SGRCodeFgRed: 664 | return kDefaultANSIColorFgRed; 665 | case AMR_SGRCodeFgGreen: 666 | return kDefaultANSIColorFgGreen; 667 | case AMR_SGRCodeFgYellow: 668 | return kDefaultANSIColorFgYellow; 669 | case AMR_SGRCodeFgBlue: 670 | return kDefaultANSIColorFgBlue; 671 | case AMR_SGRCodeFgMagenta: 672 | return kDefaultANSIColorFgMagenta; 673 | case AMR_SGRCodeFgCyan: 674 | return kDefaultANSIColorFgCyan; 675 | case AMR_SGRCodeFgWhite: 676 | return kDefaultANSIColorFgWhite; 677 | case AMR_SGRCodeFgBrightBlack: 678 | return kDefaultANSIColorFgBrightBlack; 679 | case AMR_SGRCodeFgBrightRed: 680 | return kDefaultANSIColorFgBrightRed; 681 | case AMR_SGRCodeFgBrightGreen: 682 | return kDefaultANSIColorFgBrightGreen; 683 | case AMR_SGRCodeFgBrightYellow: 684 | return kDefaultANSIColorFgBrightYellow; 685 | case AMR_SGRCodeFgBrightBlue: 686 | return kDefaultANSIColorFgBrightBlue; 687 | case AMR_SGRCodeFgBrightMagenta: 688 | return kDefaultANSIColorFgBrightMagenta; 689 | case AMR_SGRCodeFgBrightCyan: 690 | return kDefaultANSIColorFgBrightCyan; 691 | case AMR_SGRCodeFgBrightWhite: 692 | return kDefaultANSIColorFgBrightWhite; 693 | case AMR_SGRCodeBgBlack: 694 | return kDefaultANSIColorBgBlack; 695 | case AMR_SGRCodeBgRed: 696 | return kDefaultANSIColorBgRed; 697 | case AMR_SGRCodeBgGreen: 698 | return kDefaultANSIColorBgGreen; 699 | case AMR_SGRCodeBgYellow: 700 | return kDefaultANSIColorBgYellow; 701 | case AMR_SGRCodeBgBlue: 702 | return kDefaultANSIColorBgBlue; 703 | case AMR_SGRCodeBgMagenta: 704 | return kDefaultANSIColorBgMagenta; 705 | case AMR_SGRCodeBgCyan: 706 | return kDefaultANSIColorBgCyan; 707 | case AMR_SGRCodeBgWhite: 708 | return kDefaultANSIColorBgWhite; 709 | case AMR_SGRCodeBgBrightBlack: 710 | return kDefaultANSIColorBgBrightBlack; 711 | case AMR_SGRCodeBgBrightRed: 712 | return kDefaultANSIColorBgBrightRed; 713 | case AMR_SGRCodeBgBrightGreen: 714 | return kDefaultANSIColorBgBrightGreen; 715 | case AMR_SGRCodeBgBrightYellow: 716 | return kDefaultANSIColorBgBrightYellow; 717 | case AMR_SGRCodeBgBrightBlue: 718 | return kDefaultANSIColorBgBrightBlue; 719 | case AMR_SGRCodeBgBrightMagenta: 720 | return kDefaultANSIColorBgBrightMagenta; 721 | case AMR_SGRCodeBgBrightCyan: 722 | return kDefaultANSIColorBgBrightCyan; 723 | case AMR_SGRCodeBgBrightWhite: 724 | return kDefaultANSIColorBgBrightWhite; 725 | case AMR_SGRCodeNoneOrInvalid: 726 | case AMR_SGRCodeItalicOn: 727 | case AMR_SGRCodeUnderlineNone: 728 | case AMR_SGRCodeIntensityFaint: 729 | case AMR_SGRCodeAllReset: 730 | case AMR_SGRCodeBgReset: 731 | case AMR_SGRCodeFgReset: 732 | case AMR_SGRCodeIntensityBold: 733 | case AMR_SGRCodeIntensityNormal: 734 | case AMR_SGRCodeUnderlineSingle: 735 | case AMR_SGRCodeUnderlineDouble: 736 | break; 737 | } 738 | 739 | return kDefaultANSIColorFgBlack; 740 | } 741 | 742 | 743 | - (AMR_SGRCode) AMR_SGRCodeForColor:(NSColor*)aColor isForegroundColor:(BOOL)aForeground 744 | { 745 | if (self.ansiColors) 746 | { 747 | NSArray *codesForGivenColor = [self.ansiColors allKeysForObject:aColor]; 748 | 749 | if (codesForGivenColor != nil && 0 < codesForGivenColor.count) 750 | { 751 | for (NSNumber *thisCode in codesForGivenColor) 752 | { 753 | BOOL thisIsForegroundColor = (thisCode.intValue < 40); 754 | if (aForeground == thisIsForegroundColor) 755 | return thisCode.intValue; 756 | } 757 | } 758 | } 759 | 760 | if (aForeground) 761 | { 762 | if ([aColor isEqual:kDefaultANSIColorFgBlack]) 763 | return AMR_SGRCodeFgBlack; 764 | else if ([aColor isEqual:kDefaultANSIColorFgRed]) 765 | return AMR_SGRCodeFgRed; 766 | else if ([aColor isEqual:kDefaultANSIColorFgGreen]) 767 | return AMR_SGRCodeFgGreen; 768 | else if ([aColor isEqual:kDefaultANSIColorFgYellow]) 769 | return AMR_SGRCodeFgYellow; 770 | else if ([aColor isEqual:kDefaultANSIColorFgBlue]) 771 | return AMR_SGRCodeFgBlue; 772 | else if ([aColor isEqual:kDefaultANSIColorFgMagenta]) 773 | return AMR_SGRCodeFgMagenta; 774 | else if ([aColor isEqual:kDefaultANSIColorFgCyan]) 775 | return AMR_SGRCodeFgCyan; 776 | else if ([aColor isEqual:kDefaultANSIColorFgWhite]) 777 | return AMR_SGRCodeFgWhite; 778 | else if ([aColor isEqual:kDefaultANSIColorFgBrightBlack]) 779 | return AMR_SGRCodeFgBrightBlack; 780 | else if ([aColor isEqual:kDefaultANSIColorFgBrightRed]) 781 | return AMR_SGRCodeFgBrightRed; 782 | else if ([aColor isEqual:kDefaultANSIColorFgBrightGreen]) 783 | return AMR_SGRCodeFgBrightGreen; 784 | else if ([aColor isEqual:kDefaultANSIColorFgBrightYellow]) 785 | return AMR_SGRCodeFgBrightYellow; 786 | else if ([aColor isEqual:kDefaultANSIColorFgBrightBlue]) 787 | return AMR_SGRCodeFgBrightBlue; 788 | else if ([aColor isEqual:kDefaultANSIColorFgBrightMagenta]) 789 | return AMR_SGRCodeFgBrightMagenta; 790 | else if ([aColor isEqual:kDefaultANSIColorFgBrightCyan]) 791 | return AMR_SGRCodeFgBrightCyan; 792 | else if ([aColor isEqual:kDefaultANSIColorFgBrightWhite]) 793 | return AMR_SGRCodeFgBrightWhite; 794 | } 795 | else 796 | { 797 | if ([aColor isEqual:kDefaultANSIColorBgBlack]) 798 | return AMR_SGRCodeBgBlack; 799 | else if ([aColor isEqual:kDefaultANSIColorBgRed]) 800 | return AMR_SGRCodeBgRed; 801 | else if ([aColor isEqual:kDefaultANSIColorBgGreen]) 802 | return AMR_SGRCodeBgGreen; 803 | else if ([aColor isEqual:kDefaultANSIColorBgYellow]) 804 | return AMR_SGRCodeBgYellow; 805 | else if ([aColor isEqual:kDefaultANSIColorBgBlue]) 806 | return AMR_SGRCodeBgBlue; 807 | else if ([aColor isEqual:kDefaultANSIColorBgMagenta]) 808 | return AMR_SGRCodeBgMagenta; 809 | else if ([aColor isEqual:kDefaultANSIColorBgCyan]) 810 | return AMR_SGRCodeBgCyan; 811 | else if ([aColor isEqual:kDefaultANSIColorBgWhite]) 812 | return AMR_SGRCodeBgWhite; 813 | else if ([aColor isEqual:kDefaultANSIColorBgBrightBlack]) 814 | return AMR_SGRCodeBgBrightBlack; 815 | else if ([aColor isEqual:kDefaultANSIColorBgBrightRed]) 816 | return AMR_SGRCodeBgBrightRed; 817 | else if ([aColor isEqual:kDefaultANSIColorBgBrightGreen]) 818 | return AMR_SGRCodeBgBrightGreen; 819 | else if ([aColor isEqual:kDefaultANSIColorBgBrightYellow]) 820 | return AMR_SGRCodeBgBrightYellow; 821 | else if ([aColor isEqual:kDefaultANSIColorBgBrightBlue]) 822 | return AMR_SGRCodeBgBrightBlue; 823 | else if ([aColor isEqual:kDefaultANSIColorBgBrightMagenta]) 824 | return AMR_SGRCodeBgBrightMagenta; 825 | else if ([aColor isEqual:kDefaultANSIColorBgBrightCyan]) 826 | return AMR_SGRCodeBgBrightCyan; 827 | else if ([aColor isEqual:kDefaultANSIColorBgBrightWhite]) 828 | return AMR_SGRCodeBgBrightWhite; 829 | } 830 | 831 | return AMR_SGRCodeNoneOrInvalid; 832 | } 833 | 834 | 835 | 836 | // helper struct typedef and a few functions for 837 | // -closestSGRCodeForColor:isForegroundColor: 838 | 839 | typedef struct { 840 | CGFloat hue; 841 | CGFloat saturation; 842 | CGFloat brightness; 843 | } AMR_HSB; 844 | 845 | AMR_HSB makeHSB(CGFloat hue, CGFloat saturation, CGFloat brightness) 846 | { 847 | AMR_HSB outHSB; 848 | outHSB.hue = hue; 849 | outHSB.saturation = saturation; 850 | outHSB.brightness = brightness; 851 | return outHSB; 852 | } 853 | 854 | AMR_HSB getHSBFromColor(NSColor *color) 855 | { 856 | CGFloat hue = 0.0; 857 | CGFloat saturation = 0.0; 858 | CGFloat brightness = 0.0; 859 | [[color colorUsingColorSpaceName:NSCalibratedRGBColorSpace] 860 | getHue:&hue 861 | saturation:&saturation 862 | brightness:&brightness 863 | alpha:NULL 864 | ]; 865 | return makeHSB(hue, saturation, brightness); 866 | } 867 | 868 | BOOL floatsEqual(CGFloat first, CGFloat second, CGFloat maxAbsError) 869 | { 870 | return (fabs(first-second)) < maxAbsError; 871 | } 872 | 873 | #define MAX_HUE_FLOAT_EQUALITY_ABS_ERROR 0.000001 874 | 875 | - (AMR_SGRCode) closestSGRCodeForColor:(NSColor *)color isForegroundColor:(BOOL)foreground 876 | { 877 | if (color == nil) 878 | return AMR_SGRCodeNoneOrInvalid; 879 | 880 | AMR_SGRCode closestColorSGRCode = [self AMR_SGRCodeForColor:color isForegroundColor:foreground]; 881 | if (closestColorSGRCode != AMR_SGRCodeNoneOrInvalid) 882 | return closestColorSGRCode; 883 | 884 | AMR_HSB givenColorHSB = getHSBFromColor(color); 885 | 886 | CGFloat closestColorHueDiff = FLT_MAX; 887 | CGFloat closestColorSaturationDiff = FLT_MAX; 888 | CGFloat closestColorBrightnessDiff = FLT_MAX; 889 | 890 | // (background SGR codes are +10 from foreground ones:) 891 | NSUInteger AMR_SGRCodeShift = (foreground)?0:10; 892 | NSArray *ansiFgColorCodes = @[ 893 | @(AMR_SGRCodeFgBlack+AMR_SGRCodeShift), 894 | @(AMR_SGRCodeFgRed+AMR_SGRCodeShift), 895 | @(AMR_SGRCodeFgGreen+AMR_SGRCodeShift), 896 | @(AMR_SGRCodeFgYellow+AMR_SGRCodeShift), 897 | @(AMR_SGRCodeFgBlue+AMR_SGRCodeShift), 898 | @(AMR_SGRCodeFgMagenta+AMR_SGRCodeShift), 899 | @(AMR_SGRCodeFgCyan+AMR_SGRCodeShift), 900 | @(AMR_SGRCodeFgWhite+AMR_SGRCodeShift), 901 | @(AMR_SGRCodeFgBrightBlack+AMR_SGRCodeShift), 902 | @(AMR_SGRCodeFgBrightRed+AMR_SGRCodeShift), 903 | @(AMR_SGRCodeFgBrightGreen+AMR_SGRCodeShift), 904 | @(AMR_SGRCodeFgBrightYellow+AMR_SGRCodeShift), 905 | @(AMR_SGRCodeFgBrightBlue+AMR_SGRCodeShift), 906 | @(AMR_SGRCodeFgBrightMagenta+AMR_SGRCodeShift), 907 | @(AMR_SGRCodeFgBrightCyan+AMR_SGRCodeShift), 908 | @(AMR_SGRCodeFgBrightWhite+AMR_SGRCodeShift), 909 | ]; 910 | for (NSNumber *thisSGRCodeNumber in ansiFgColorCodes) 911 | { 912 | AMR_SGRCode thisSGRCode = thisSGRCodeNumber.intValue; 913 | NSColor *thisColor = [self colorForSGRCode:thisSGRCode]; 914 | 915 | AMR_HSB thisColorHSB = getHSBFromColor(thisColor); 916 | 917 | CGFloat hueDiff = fabs(givenColorHSB.hue - thisColorHSB.hue); 918 | CGFloat saturationDiff = fabs(givenColorHSB.saturation - thisColorHSB.saturation); 919 | CGFloat brightnessDiff = fabs(givenColorHSB.brightness - thisColorHSB.brightness); 920 | 921 | // comparison depends on hue, saturation and brightness 922 | // (strictly in that order): 923 | 924 | if (!floatsEqual(hueDiff, closestColorHueDiff, MAX_HUE_FLOAT_EQUALITY_ABS_ERROR)) 925 | { 926 | if (hueDiff > closestColorHueDiff) 927 | continue; 928 | closestColorSGRCode = thisSGRCode; 929 | closestColorHueDiff = hueDiff; 930 | closestColorSaturationDiff = saturationDiff; 931 | closestColorBrightnessDiff = brightnessDiff; 932 | continue; 933 | } 934 | 935 | if (!floatsEqual(saturationDiff, closestColorSaturationDiff, MAX_HUE_FLOAT_EQUALITY_ABS_ERROR)) 936 | { 937 | if (saturationDiff > closestColorSaturationDiff) 938 | continue; 939 | closestColorSGRCode = thisSGRCode; 940 | closestColorHueDiff = hueDiff; 941 | closestColorSaturationDiff = saturationDiff; 942 | closestColorBrightnessDiff = brightnessDiff; 943 | continue; 944 | } 945 | 946 | if (!floatsEqual(brightnessDiff, closestColorBrightnessDiff, MAX_HUE_FLOAT_EQUALITY_ABS_ERROR)) 947 | { 948 | if (brightnessDiff > closestColorBrightnessDiff) 949 | continue; 950 | closestColorSGRCode = thisSGRCode; 951 | closestColorHueDiff = hueDiff; 952 | closestColorSaturationDiff = saturationDiff; 953 | closestColorBrightnessDiff = brightnessDiff; 954 | continue; 955 | } 956 | 957 | // If hue (especially hue!), saturation and brightness diffs all 958 | // are equal to some other color, we need to prefer one or the 959 | // other so we'll select the more 'distinctive' color of the 960 | // two (this is *very* subjective, obviously). I basically just 961 | // looked at the hue chart, went through all the points between 962 | // our main ANSI colors and decided which side the middle point 963 | // would lean on. (e.g. the purple color that is exactly between 964 | // the blue and magenta ANSI colors looks more magenta than 965 | // blue to me so I put magenta higher than blue in the list 966 | // below.) 967 | // 968 | // subjective ordering of colors from most to least 'distinctive': 969 | int colorDistinctivenessOrder[6] = { 970 | AMR_SGRCodeFgRed+(int)AMR_SGRCodeShift, 971 | AMR_SGRCodeFgMagenta+(int)AMR_SGRCodeShift, 972 | AMR_SGRCodeFgBlue+(int)AMR_SGRCodeShift, 973 | AMR_SGRCodeFgGreen+(int)AMR_SGRCodeShift, 974 | AMR_SGRCodeFgCyan+(int)AMR_SGRCodeShift, 975 | AMR_SGRCodeFgYellow+(int)AMR_SGRCodeShift 976 | }; 977 | for (int i = 0; i < 6; i++) 978 | { 979 | if (colorDistinctivenessOrder[i] == closestColorSGRCode) 980 | break; 981 | else if (colorDistinctivenessOrder[i] == thisSGRCode) 982 | { 983 | closestColorSGRCode = thisSGRCode; 984 | closestColorHueDiff = hueDiff; 985 | closestColorSaturationDiff = saturationDiff; 986 | closestColorBrightnessDiff = brightnessDiff; 987 | } 988 | } 989 | } 990 | 991 | return closestColorSGRCode; 992 | } 993 | 994 | 995 | 996 | @end 997 | -------------------------------------------------------------------------------- /Widget/AMR_ANSIEscapeHelper/readme.txt: -------------------------------------------------------------------------------- 1 | 2 | DESCRIPTION: 3 | -------------------------------- 4 | 5 | ANSIEscapeHelper is an Objective-C class for dealing with ANSI escape 6 | sequences. Its main purpose is to translate between NSStrings 7 | that contain ANSI escape sequences and similarly formatted 8 | NSAttributedStrings. 9 | 10 | The headerdoc directory contains the API documentation in HTML format. 11 | 12 | If you fix something in this code or use it in your software, please 13 | let me know -- I'm curious ;). You can contact me at http://hasseg.org. 14 | Thanks. 15 | 16 | 17 | Copyright 2009 Ali Rantakari 18 | http://hasseg.org/ansiEscapeHelper 19 | 20 | 21 | 22 | 23 | 24 | LICENSE: 25 | -------------------------------- 26 | 27 | The MIT License 28 | 29 | Copyright (c) 2009 Ali Rantakari 30 | 31 | Permission is hereby granted, free of charge, to any person obtaining a copy 32 | of this software and associated documentation files (the "Software"), to deal 33 | in the Software without restriction, including without limitation the rights 34 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 35 | copies of the Software, and to permit persons to whom the Software is 36 | furnished to do so, subject to the following conditions: 37 | 38 | The above copyright notice and this permission notice shall be included in 39 | all copies or substantial portions of the Software. 40 | 41 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 42 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 43 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 44 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 45 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 46 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 47 | THE SOFTWARE. 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Widget/EditViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // SearchViewController.h 3 | // Today Scripts 4 | // 5 | // Created by Sam Rothenberg on 10/21/14. 6 | // Copyright (c) 2014 Sam Rothenberg. All rights reserved. 7 | // 8 | 9 | #import "TodayScript.h" 10 | 11 | @class EditViewController; 12 | 13 | 14 | @interface EditViewLabelField : NSTextView 15 | @property IBOutlet EditViewController *editViewController; 16 | @end 17 | 18 | @interface EditViewProgramField : NSTextView 19 | @property IBOutlet EditViewController *editViewController; 20 | @end 21 | 22 | @interface EditViewScriptView : NSTextView; 23 | @property IBOutlet EditViewController *editViewController; 24 | @end 25 | 26 | @interface EditViewAutoRunButton : NSButton 27 | @property IBOutlet EditViewController *editViewController; 28 | @end 29 | 30 | @interface EditViewShowStatusButton : NSButton 31 | @property IBOutlet EditViewController *editViewController; 32 | @end 33 | 34 | @interface EditViewSaveButton : NSButton 35 | @property IBOutlet EditViewController *editViewController; 36 | @end 37 | 38 | 39 | @interface EditViewController : NCWidgetSearchViewController 40 | 41 | - (void)editScript:(TodayScript *)script; 42 | - (void)createScript; 43 | - (void)cancelScript; 44 | 45 | @property IBOutlet EditViewLabelField *labelField; 46 | @property IBOutlet EditViewProgramField *programField; 47 | @property IBOutlet EditViewScriptView *scriptField; 48 | @property IBOutlet EditViewAutoRunButton *autoRunButton; 49 | @property IBOutlet EditViewShowStatusButton *showStatusButton; 50 | @property IBOutlet EditViewSaveButton *saveButton; 51 | 52 | @end 53 | -------------------------------------------------------------------------------- /Widget/EditViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // SearchViewController.m 3 | // Today Scripts 4 | // 5 | // Created by Sam Rothenberg on 10/21/14. 6 | // Copyright (c) 2014 Sam Rothenberg. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "EditViewController.h" 12 | #import "TodayViewController.h" 13 | 14 | @implementation EditViewController 15 | { 16 | TodayScript *script; 17 | } 18 | 19 | - (void)viewDidLoad 20 | { 21 | [super viewDidLoad]; 22 | 23 | self.labelField.backgroundColor = NSColor.clearColor; 24 | self.programField.backgroundColor = NSColor.clearColor; 25 | self.scriptField.backgroundColor = NSColor.clearColor; 26 | 27 | self.labelField.textColor = NSColor.labelColor; 28 | self.programField.textColor = NSColor.labelColor; 29 | self.scriptField.textColor = NSColor.labelColor; 30 | 31 | self.labelField.insertionPointColor = NSColor.labelColor; 32 | self.programField.insertionPointColor = NSColor.labelColor; 33 | self.scriptField.insertionPointColor = NSColor.labelColor; 34 | 35 | self.labelField.font = [NSFont boldSystemFontOfSize:11]; 36 | self.programField.font = [NSFont boldSystemFontOfSize:11]; 37 | self.scriptField.font = [NSFont fontWithName:@"Menlo-Bold" size:9.5]; 38 | 39 | self.labelField.textContainerInset = NSMakeSize(0, 2); 40 | self.programField.textContainerInset = NSMakeSize(0, 2); 41 | self.scriptField.textContainerInset = NSMakeSize(0, 2); 42 | 43 | NSDictionary *buttonAttributes = @{ 44 | NSForegroundColorAttributeName: NSColor.labelColor, 45 | NSFontAttributeName: [NSFont systemFontOfSize:11] 46 | }; 47 | self.autoRunButton.attributedTitle = [[NSAttributedString alloc] 48 | initWithString:self.autoRunButton.title attributes:buttonAttributes]; 49 | self.showStatusButton.attributedTitle = [[NSAttributedString alloc] 50 | initWithString:self.showStatusButton.title attributes:buttonAttributes]; 51 | self.saveButton.attributedTitle = [[NSAttributedString alloc] 52 | initWithString:self.saveButton.title attributes:buttonAttributes]; 53 | 54 | // Disable all substitutions in the script field. 55 | self.scriptField.automaticDashSubstitutionEnabled = NO; 56 | self.scriptField.automaticQuoteSubstitutionEnabled = NO; 57 | self.scriptField.automaticTextReplacementEnabled = NO; 58 | self.scriptField.automaticLinkDetectionEnabled = NO; 59 | self.scriptField.automaticSpellingCorrectionEnabled = NO; 60 | self.scriptField.automaticDataDetectionEnabled = NO; 61 | } 62 | 63 | - (void)editScript:(TodayScript *)existingScript 64 | { 65 | // Show ourselves in the widget. 66 | [todayViewController presentViewControllerInWidget:self]; 67 | // Set the button's title to designate that we are editing a script. 68 | self.saveButton.title = @"Save Script"; 69 | 70 | // Set our script variable to the script passed to us. 71 | script = existingScript; 72 | 73 | // Set the values in our form to those of the script. 74 | self.labelField.string = script.label; 75 | self.programField.string = script.program; 76 | self.scriptField.string = script.script; 77 | self.autoRunButton.state = script.autoRun ? NSOnState : NSOffState; 78 | self.showStatusButton.state = script.showStatus ? NSOnState : NSOffState; 79 | 80 | // Focus the script field initially. 81 | [self.view.window makeFirstResponder:self.scriptField]; 82 | } 83 | 84 | - (void)createScript 85 | { 86 | // We will not be working with an existing script. 87 | script = nil; 88 | 89 | // Show ourselves in the widget. 90 | [todayViewController presentViewControllerInWidget:self]; 91 | // Set the button's title to designate that we are creating a script. 92 | self.saveButton.title = @"Add Script"; 93 | 94 | // Set up our fields with the default values. 95 | self.labelField.string = @""; 96 | self.programField.string = NSProcessInfo.processInfo.environment[@"SHELL"]; 97 | self.scriptField.string = @""; 98 | self.autoRunButton.state = NSOnState; 99 | self.showStatusButton.state = NSOnState; 100 | 101 | // Focus the label field initially. 102 | [self.view.window makeFirstResponder:self.labelField]; 103 | } 104 | 105 | // Method invoked when user presses the "Add Script" button. 106 | - (IBAction)saveScript:(id)sender 107 | { 108 | // If the interpreter is not a valid executable file, style the text to 109 | // indicate the error to the user, then abort. 110 | NSString *programString = self.programField.string; 111 | if (! [NSFileManager.defaultManager isExecutableFileAtPath:programString]) { 112 | self.programField.textColor = [NSColor colorWithRed:1.0 green:0.2 blue:0.2 alpha:1.0]; 113 | return; 114 | } 115 | 116 | // If we were not given an existing dictionary to modify, set that up to 117 | // work with. Otherwise, create a new one. 118 | TodayScript *newScript = script ?: [[TodayScript alloc] init]; 119 | 120 | newScript.program = programString.copy; 121 | 122 | // Set the script to the dictionary if the user provided one. Otherwise, 123 | // remove any which may have previously existed. 124 | newScript.script = self.scriptField.string.copy; 125 | 126 | // Set the script's title to the user provided one. 127 | newScript.label = self.labelField.string.copy; 128 | // If a title was not provided, use the text of the script, or the name of 129 | // the program itself if there is no script. 130 | if (! newScript.label.length) 131 | newScript.label = newScript.script.length ? newScript.script : newScript.program; 132 | 133 | // If the checkbox wasn't unchecked, this script is to be run automatically. 134 | newScript.autoRun = (self.autoRunButton.state != NSOffState); 135 | 136 | // If the checkbox wasn't unchecked, this script is to be run automatically. 137 | newScript.showStatus = (self.showStatusButton.state != NSOffState); 138 | 139 | // If we were given a script to work with, remove it from our form, make 140 | // sure it it's stopped running, then update our defaults. 141 | if (script) 142 | { 143 | script = nil; 144 | [newScript terminate]; 145 | [TodayScriptArray.sharedScripts saveDefaults]; 146 | } 147 | // Otherwise, add the new script to the list array and update our list view. 148 | else { 149 | [todayViewController.arrayController addObject:newScript]; 150 | todayViewController.listViewController.contents = 151 | todayViewController.arrayController.arrangedObjects; 152 | } 153 | // Hide ourselves. 154 | [todayViewController dismissViewController:self]; 155 | 156 | // If the newly saved script is set to run automatically, do so now. 157 | if (newScript.autoRun) 158 | [newScript run]; 159 | } 160 | 161 | - (void)cancelScript 162 | { 163 | script = nil; 164 | [todayViewController dismissViewController:self]; 165 | } 166 | 167 | @end 168 | 169 | 170 | 171 | @implementation EditViewLabelField 172 | 173 | - (void)keyDown:(NSEvent *)theEvent 174 | { 175 | // Get the character that was typed. 176 | int character = [theEvent.characters characterAtIndex:0]; 177 | 178 | // If it was a tab with the shift key, we will be moving back a field. 179 | if (character == NSBackTabCharacter) 180 | { 181 | // If full keyboard navigation is enabled, move back to the save button. 182 | if (self.editViewController.saveButton.canBecomeKeyView) 183 | { 184 | self.selectedRange = NSMakeRange(0, 0); 185 | [self.window makeFirstResponder:self.editViewController.saveButton]; 186 | } 187 | // If it is not, move back to the script field. 188 | else { 189 | self.selectedRange = NSMakeRange(0, 0); 190 | [self.window makeFirstResponder:self.editViewController.scriptField]; 191 | } 192 | } 193 | 194 | // If it was a tab without the shift key, move on to the script field. 195 | else if (character == NSTabCharacter) 196 | { 197 | self.selectedRange = NSMakeRange(0, 0); 198 | [self.window makeFirstResponder:self.editViewController.programField]; 199 | [self.editViewController.programField selectAll:self]; 200 | } 201 | 202 | // If the character wasn't a tab, pass it to the text field normally. 203 | else 204 | [super keyDown:theEvent]; 205 | } 206 | 207 | @end 208 | 209 | 210 | @implementation EditViewProgramField 211 | 212 | - (void)didChangeText 213 | { 214 | [super didChangeText]; 215 | 216 | // When the user starts editing the program field, make sure the text color 217 | // returns to normal in case it was previously changed to indicate an error. 218 | if ([NSFileManager.defaultManager isExecutableFileAtPath:self.string]) 219 | self.textColor = NSColor.labelColor; 220 | 221 | // If user enters an invalid program in the program field, set its text as 222 | // red to indicate this. 223 | else 224 | self.textColor = [NSColor colorWithRed:1.0 green:0.5 blue:0.5 alpha:1.0]; 225 | } 226 | 227 | - (void)keyDown:(NSEvent *)theEvent 228 | { 229 | // Get the character that was typed. 230 | int character = [theEvent.characters characterAtIndex:0]; 231 | 232 | // If the shift and tab were typed, move back to the program field. 233 | if (character == NSBackTabCharacter) 234 | { 235 | self.selectedRange = NSMakeRange(0, 0); 236 | [self.window makeFirstResponder:self.editViewController.labelField]; 237 | [self.editViewController.labelField selectAll:self]; 238 | } 239 | 240 | // If just a tab was typed, move on to the show status box. 241 | else if (character == NSTabCharacter) 242 | { 243 | self.selectedRange = NSMakeRange(0, 0); 244 | [self.window makeFirstResponder:self.editViewController.scriptField]; 245 | } 246 | 247 | // For any other keys, pass them to the text field as normal. 248 | else 249 | [super keyDown:theEvent]; 250 | } 251 | 252 | @end 253 | 254 | 255 | @implementation EditViewScriptView 256 | 257 | - (void)keyDown:(NSEvent *)theEvent 258 | { 259 | // Get the character that was typed. 260 | int character = [theEvent.characters characterAtIndex:0]; 261 | 262 | // If the shift and tab were typed, move back to the program field. 263 | if (character == NSBackTabCharacter) 264 | { 265 | [self.window makeFirstResponder:self.editViewController.programField]; 266 | [self.editViewController.programField selectAll:self]; 267 | } 268 | 269 | // If option and tab were typed, we will be moving forward. 270 | else if (character == NSTabCharacter && (theEvent.modifierFlags & NSAlternateKeyMask)) 271 | { 272 | // If full keyboard navigation is enabled, move on to the auto-run box. 273 | if (self.editViewController.autoRunButton.canBecomeKeyView) 274 | [self.window makeFirstResponder:self.editViewController.autoRunButton]; 275 | // If it isn't, move on to the label field. 276 | else { 277 | [self.window makeFirstResponder:self.editViewController.labelField]; 278 | [self.editViewController.labelField selectAll:self]; 279 | } 280 | } 281 | // For any other keys, pass them to the text field as normal. 282 | else 283 | [super keyDown:theEvent]; 284 | } 285 | 286 | @end 287 | 288 | 289 | @implementation EditViewAutoRunButton 290 | 291 | - (void)keyDown:(NSEvent *)theEvent 292 | { 293 | // Get the character that was typed. 294 | int character = [theEvent.characters characterAtIndex:0]; 295 | 296 | // If the shift and tab were typed, move back to the script field. 297 | if (character == NSBackTabCharacter) 298 | [self.window makeFirstResponder:self.editViewController.scriptField]; 299 | 300 | // If just a tab was typed, move on to the show status box. 301 | else if (character == NSTabCharacter) 302 | [self.window makeFirstResponder:self.editViewController.showStatusButton]; 303 | 304 | // If the space key was typed, toggle our state. 305 | else if (character == ' ') 306 | [self performClick:self]; 307 | 308 | // Any other keys, pass to the box to handle. 309 | else 310 | [super keyDown:theEvent]; 311 | } 312 | 313 | @end 314 | 315 | 316 | @implementation EditViewShowStatusButton 317 | 318 | - (void)keyDown:(NSEvent *)theEvent 319 | { 320 | // Get the character that was typed. 321 | int character = [theEvent.characters characterAtIndex:0]; 322 | 323 | // If the shift and tab were typed, move back to the script field. 324 | if (character == NSBackTabCharacter) 325 | [self.window makeFirstResponder:self.editViewController.autoRunButton]; 326 | 327 | // If just a tab was typed, move on to the show status box. 328 | else if (character == NSTabCharacter) 329 | [self.window makeFirstResponder:self.editViewController.saveButton]; 330 | 331 | // If the space key was typed, toggle our state. 332 | else if (character == ' ') 333 | [self performClick:self]; 334 | 335 | // Any other keys, pass to the box to handle. 336 | else 337 | [super keyDown:theEvent]; 338 | } 339 | 340 | @end 341 | 342 | 343 | @implementation EditViewSaveButton 344 | 345 | - (void)keyDown:(NSEvent *)theEvent 346 | { 347 | // Get the character that was typed. 348 | int character = [theEvent.characters characterAtIndex:0]; 349 | 350 | // If the shift and tab were typed, move back to the script field. 351 | if (character == NSBackTabCharacter) 352 | [self.window makeFirstResponder:self.editViewController.showStatusButton]; 353 | 354 | // If just a tab was typed, move on to the show status box. 355 | else if (character == NSTabCharacter) 356 | { 357 | [self.window makeFirstResponder:self.editViewController.labelField]; 358 | [self.editViewController.labelField selectAll:self]; 359 | } 360 | // If the space key was typed, toggle our state. 361 | else if (character == ' ') 362 | [self performClick:self]; 363 | 364 | // Any other keys, pass to the box to handle. 365 | else 366 | [super keyDown:theEvent]; 367 | } 368 | 369 | @end -------------------------------------------------------------------------------- /Widget/EditViewController.xib: -------------------------------------------------------------------------------- 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 | 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 | 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 | 138 | 139 | 140 | 141 | 142 | 143 | 153 | 163 | 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 | -------------------------------------------------------------------------------- /Widget/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /Widget/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Widget 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | org.samroth.Today-Scripts.$(PRODUCT_NAME:rfc1034identifier) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | XPC! 19 | CFBundleShortVersionString 20 | 1.1 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSExtension 28 | 29 | NSExtensionPointIdentifier 30 | com.apple.widget-extension 31 | NSExtensionPrincipalClass 32 | TodayViewController 33 | com.apple.notificationcenter.widget.description 34 | Widget 35 | 36 | NSHumanReadableCopyright 37 | Copyright © 2014 Sam Rothenberg. All rights reserved. 38 | 39 | 40 | -------------------------------------------------------------------------------- /Widget/ListRowViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ListRowViewController.h 3 | // Scripts 4 | // 5 | // Created by Sam Rothenberg on 8/14/14. 6 | // Copyright (c) 2014 Sam Rothenberg. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class ListRowViewController; 12 | 13 | 14 | @interface ListRowLabelButton : NSButton 15 | @property IBOutlet ListRowViewController *listRowViewController; 16 | @end 17 | 18 | @interface ListRowEditButton : NSButton 19 | @property IBOutlet ListRowViewController *listRowViewController; 20 | @end 21 | 22 | @interface ListRowOutputView : NSTextView 23 | @property IBOutlet ListRowViewController *listRowViewController; 24 | @end 25 | 26 | 27 | @interface ListRowViewController : NSViewController 28 | 29 | // Our UI objects. 30 | @property IBOutlet ListRowLabelButton *labelButton; 31 | @property IBOutlet ListRowEditButton *editButton; 32 | @property IBOutlet ListRowOutputView *outputView; 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /Widget/ListRowViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ListRowViewController.m 3 | // Scripts 4 | // 5 | // Created by Sam Rothenberg on 8/14/14. 6 | // Copyright (c) 2014 Sam Rothenberg. All rights reserved. 7 | // 8 | 9 | #import "ListRowViewController.h" 10 | #import "TodayScript.h" 11 | #import "TodayViewController.h" 12 | #import "EditViewController.h" 13 | 14 | @implementation ListRowViewController 15 | 16 | - (NSString *)nibName { 17 | return @"ListRowViewController"; 18 | } 19 | 20 | - (void)loadView 21 | { 22 | [super loadView]; 23 | 24 | // Make the text cursor for the output view (mostly) invisible. 25 | self.outputView.insertionPointColor = NSColor.clearColor; 26 | // Make the edit button dim. 27 | self.editButton.alphaValue = 0.1; 28 | 29 | // Give the list view a moment to display the content, then reset the focus. 30 | dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, 250 * NSEC_PER_MSEC); 31 | dispatch_after(delay, dispatch_get_main_queue(), ^{ 32 | [self.view.window makeFirstResponder:todayViewController.view]; 33 | }); 34 | } 35 | 36 | - (BOOL)textShouldBeginEditing:(NSText *)textObject { 37 | // Prevent editing of the output field. 38 | return NO; 39 | } 40 | 41 | 42 | - (IBAction)startOrStop:(id)sender 43 | { 44 | // Get the current script we represent. 45 | TodayScript *script = self.representedObject; 46 | // Otherwise if the script is not running, we are being asked to run it. 47 | if (script.running) 48 | [script terminate]; 49 | // If it is running however, then we are being asked to terminate it. 50 | else 51 | [script run]; 52 | } 53 | 54 | - (IBAction)edit:(id)sender { 55 | // Get the current script we represent and tell the editor to open it. 56 | [todayViewController.editViewController editScript:(TodayScript *)self.representedObject]; 57 | } 58 | 59 | @end 60 | 61 | 62 | 63 | @implementation ListRowLabelButton 64 | 65 | - (void)setTitle:(NSString *)title 66 | { 67 | super.attributedTitle = [[NSAttributedString alloc] initWithString:title attributes:@{ 68 | NSForegroundColorAttributeName: NSColor.labelColor, 69 | NSFontAttributeName: [NSFont boldSystemFontOfSize:11] 70 | }]; 71 | } 72 | 73 | - (void)keyDown:(NSEvent *)theEvent 74 | { 75 | // Get the character that was typed. 76 | int character = [theEvent.characters characterAtIndex:0]; 77 | 78 | // If shift and tab were typed, we will be moving to the previous view. 79 | if (character == NSBackTabCharacter) 80 | { 81 | // Get the default previous view. 82 | NSView *previousKeyView = self.previousValidKeyView; 83 | // If the default view is an output view's enclosing scroll view, keep 84 | // going to the output view it contains. 85 | if ([previousKeyView isKindOfClass:NSScrollView.class]) 86 | previousKeyView = previousKeyView.previousValidKeyView.previousValidKeyView; 87 | 88 | // If the previous view is another output view, select all of its text. 89 | if ([previousKeyView isKindOfClass:ListRowOutputView.class]) 90 | [previousKeyView selectAll:self]; 91 | 92 | // Set the focus to the new view. 93 | [self.window makeFirstResponder:previousKeyView]; 94 | } 95 | 96 | // Any other keys, pass to the box to handle. 97 | else 98 | [super keyDown:theEvent]; 99 | } 100 | 101 | @end 102 | 103 | 104 | @implementation ListRowEditButton 105 | 106 | - (void)keyDown:(NSEvent *)theEvent 107 | { 108 | // If just a tab was typed, move on to the show status box. 109 | if ([theEvent.characters characterAtIndex:0] == NSTabCharacter) 110 | { 111 | // Get the default next view. 112 | NSView *nextKeyView = self.nextValidKeyView; 113 | // If the default view is an output view's enclosing scroll view, keep 114 | // going to the output view it contains. 115 | if ([nextKeyView isKindOfClass:NSScrollView.class]) 116 | nextKeyView = nextKeyView.nextValidKeyView.nextValidKeyView; 117 | 118 | // By now we should have an output view. Select its contents. 119 | [nextKeyView selectAll:self]; 120 | 121 | // Set the focus to the new view. 122 | [self.window makeFirstResponder:nextKeyView]; 123 | } 124 | // // If the enter, space, or return keys were typed, save the script. 125 | // else if (character == NSEnterCharacter || character == ' ' || character == '\r') 126 | // [self performClick:self]; 127 | 128 | // Any other keys, pass to the box to handle. 129 | else 130 | [super keyDown:theEvent]; 131 | } 132 | 133 | @end 134 | 135 | 136 | @implementation ListRowOutputView 137 | 138 | - (BOOL)canBecomeKeyView { 139 | return YES; 140 | } 141 | 142 | - (void)keyDown:(NSEvent *)theEvent 143 | { 144 | // Get the character that was typed. 145 | int character = [theEvent.characters characterAtIndex:0]; 146 | 147 | // If shift and tab were typed, we will be moving to the previous view. 148 | if (character == NSBackTabCharacter) 149 | { 150 | // Remove our own selection. 151 | self.selectedRange = NSMakeRange(0, 0); 152 | 153 | // Get the default previous view. 154 | NSView *previousKeyView = self.previousValidKeyView; 155 | // If the default view is our enclosing clip view, keep going back to 156 | // select its enclosing scroll view's previous view. 157 | if ([previousKeyView isKindOfClass:NSClipView.class]) 158 | previousKeyView = previousKeyView.previousValidKeyView.previousValidKeyView; 159 | 160 | // If the previous view is another output view, select all of its text. 161 | else if ([previousKeyView isKindOfClass:ListRowOutputView.class]) 162 | [previousKeyView selectAll:self]; 163 | 164 | // Set the focus to the new view. 165 | [self.window makeFirstResponder:previousKeyView]; 166 | } 167 | 168 | // If just tab was typed, we will be selecting the next view. 169 | else if (character == NSTabCharacter) 170 | { 171 | // Remove our own selection. 172 | self.selectedRange = NSMakeRange(0, 0); 173 | 174 | // Get the default next view. 175 | NSView *nextKeyView = self.nextValidKeyView; 176 | 177 | // If the next view is another output view, select all of its text. 178 | if ([nextKeyView isKindOfClass:ListRowOutputView.class]) 179 | [nextKeyView selectAll:self]; 180 | 181 | // Set the focus to the new view. 182 | [self.window makeFirstResponder:nextKeyView]; 183 | } 184 | 185 | // For any other keys, pass them to the text field as normal. 186 | else 187 | [super keyDown:theEvent]; 188 | } 189 | 190 | @end 191 | -------------------------------------------------------------------------------- /Widget/ListRowViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 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 | NSNegateBoolean 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 | NSNegateBoolean 101 | 102 | 103 | 104 | 105 | 106 | 107 | NSNegateBoolean 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 | NSNegateBoolean 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 | 176 | 180 | 181 | 196 | 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 | YnBsaXN0MDDUAQIDBAUGPj9YJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoK4HCBMU 241 | GR4fIyQsLzI4O1UkbnVsbNUJCgsMDQ4PEBESVk5TU2l6ZVYkY2xhc3NcTlNJbWFnZUZsYWdzVk5TUmVw 242 | c1dOU0NvbG9ygAKADRIgwwAAgAOAC1Z7MSwgMX3SFQoWGFpOUy5vYmplY3RzoReABIAK0hUKGh2iGxyA 243 | BYAGgAkQANIgCiEiXxAUTlNUSUZGUmVwcmVzZW50YXRpb26AB4AITxEIdE1NACoAAAAKAAAADgEAAAMA 244 | AAABAAEAAAEBAAMAAAABAAEAAAECAAMAAAACAAgACAEDAAMAAAABAAEAAAEGAAMAAAABAAEAAAERAAQA 245 | AAABAAAACAESAAMAAAABAAEAAAEVAAMAAAABAAIAAAEWAAMAAAABAAEAAAEXAAQAAAABAAAAAgEcAAMA 246 | AAABAAEAAAFSAAMAAAABAAEAAAFTAAMAAAACAAEAAYdzAAcAAAe8AAAAuAAAAAAAAAe8YXBwbAIgAABt 247 | bnRyR1JBWVhZWiAH0AACAA4ADAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYA 248 | AQAAAADTLWFwcGwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVk 249 | ZXNjAAAAwAAAAG9kc2NtAAABMAAABi5jcHJ0AAAHYAAAADh3dHB0AAAHmAAAABRrVFJDAAAHrAAAAA5k 250 | ZXNjAAAAAAAAABVHZW5lcmljIEdyYXkgUHJvZmlsZQAAAAAAAAAAAAAAFUdlbmVyaWMgR3JheSBQcm9m 251 | aWxlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbWx1YwAAAAAA 252 | AAAeAAAADHNrU0sAAAAqAAABeGRhREsAAAA0AAABomNhRVMAAAAsAAAB1nB0QlIAAAAqAAACAnVrVUEA 253 | AAAsAAACLGZyRlUAAAAqAAACWGh1SFUAAAAuAAACgnpoVFcAAAAQAAACsG5iTk8AAAAsAAACwGNzQ1oA 254 | AAAkAAAC7GhlSUwAAAAgAAADEGl0SVQAAAAuAAADMHJvUk8AAAAkAAADXmRlREUAAAA6AAADgmtvS1IA 255 | AAAYAAADvHN2U0UAAAAuAAAD1HpoQ04AAAAQAAAEAmphSlAAAAAWAAAEEmVsR1IAAAAkAAAEKHB0UE8A 256 | AAA4AAAETG5sTkwAAAAqAAAEhGVzRVMAAAAoAAAErnRoVEgAAAAkAAAE1nRyVFIAAAAiAAAE+mZpRkkA 257 | AAAsAAAFHGhySFIAAAA6AAAFSHBsUEwAAAA2AAAFgnJ1UlUAAAAmAAAFuGFyRUcAAAAoAAAF3mVuVVMA 258 | AAAoAAAGBgBWAWEAZQBvAGIAZQBjAG4A/QAgAHMAaQB2AP0AIABwAHIAbwBmAGkAbABHAGUAbgBlAHIA 259 | ZQBsACAAZwByAOUAdABvAG4AZQBiAGUAcwBrAHIAaQB2AGUAbABzAGUAUABlAHIAZgBpAGwAIABkAGUA 260 | IABnAHIAaQBzACAAZwBlAG4A6AByAGkAYwBQAGUAcgBmAGkAbAAgAEMAaQBuAHoAYQAgAEcAZQBuAOkA 261 | cgBpAGMAbwQXBDAEMwQwBDsETAQ9BDgEOQAgBD8EQAQ+BEQEMAQ5BDsAIABHAHIAYQB5AFAAcgBvAGYA 262 | aQBsACAAZwDpAG4A6QByAGkAcQB1AGUAIABnAHIAaQBzAMEAbAB0AGEAbADhAG4AbwBzACAAcwB6APwA 263 | cgBrAGUAIABwAHIAbwBmAGkAbJAadShwcJaOgnJfaWPPj/AARwBlAG4AZQByAGkAcwBrACAAZwByAOUA 264 | dABvAG4AZQBwAHIAbwBmAGkAbABPAGIAZQBjAG4A/QAgAWEAZQBkAP0AIABwAHIAbwBmAGkAbAXkBegF 265 | 1QXkBdkF3AAgAEcAcgBhAHkAIAXbBdwF3AXZAFAAcgBvAGYAaQBsAG8AIABnAHIAaQBnAGkAbwAgAGcA 266 | ZQBuAGUAcgBpAGMAbwBQAHIAbwBmAGkAbAAgAGcAcgBpACAAZwBlAG4AZQByAGkAYwBBAGwAbABnAGUA 267 | bQBlAGkAbgBlAHMAIABHAHIAYQB1AHMAdAB1AGYAZQBuAC0AUAByAG8AZgBpAGzHfLwYACAARwByAGEA 268 | eQAg1QS4XNMMx3wARwBlAG4AZQByAGkAcwBrACAAZwByAOUAcwBrAGEAbABlAHAAcgBvAGYAaQBsZm6Q 269 | GnBwXqZjz4/wZYdO9k4AgiwwsDDsMKQw1zDtMNUwoTCkMOsDkwO1A70DuQO6A8wAIAPAA8EDvwPGA68D 270 | uwAgA7MDugPBA7kAUABlAHIAZgBpAGwAIABnAGUAbgDpAHIAaQBjAG8AIABkAGUAIABjAGkAbgB6AGUA 271 | bgB0AG8AcwBBAGwAZwBlAG0AZQBlAG4AIABnAHIAaQBqAHMAcAByAG8AZgBpAGUAbABQAGUAcgBmAGkA 272 | bAAgAGcAcgBpAHMAIABnAGUAbgDpAHIAaQBjAG8OQg4bDiMORA4fDiUOTA4qDjUOQA4XDjIOFw4xDkgO 273 | Jw5EDhsARwBlAG4AZQBsACAARwByAGkAIABQAHIAbwBmAGkAbABpAFkAbABlAGkAbgBlAG4AIABoAGEA 274 | cgBtAGEAYQBwAHIAbwBmAGkAaQBsAGkARwBlAG4AZQByAGkBDQBrAGkAIABwAHIAbwBmAGkAbAAgAHMA 275 | aQB2AGkAaAAgAHQAbwBuAG8AdgBhAFUAbgBpAHcAZQByAHMAYQBsAG4AeQAgAHAAcgBvAGYAaQBsACAA 276 | cwB6AGEAcgBvAVsAYwBpBB4EMQRJBDgEOQAgBEEENQRABEsEOQAgBD8EQAQ+BEQEOAQ7BEwGRQZEBkEA 277 | IAYqBjkGMQZKBkEAIABHAHIAYQB5ACAGJwZEBjkGJwZFAEcAZQBuAGUAcgBpAGMAIABHAHIAYQB5ACAA 278 | UAByAG8AZgBpAGwAZQAAdGV4dAAAAABDb3B5cmlnaHQgMjAwNyBBcHBsZSBJbmMuLCBhbGwgcmlnaHRz 279 | IHJlc2VydmVkLgBYWVogAAAAAAAA81EAAQAAAAEWzGN1cnYAAAAAAAAAAQHNAADSJSYnKFokY2xhc3Nu 280 | YW1lWCRjbGFzc2VzXxAQTlNCaXRtYXBJbWFnZVJlcKMpKitfEBBOU0JpdG1hcEltYWdlUmVwWk5TSW1h 281 | Z2VSZXBYTlNPYmplY3TSJSYtLldOU0FycmF5oi0r0iUmMDFeTlNNdXRhYmxlQXJyYXmjMC0r0zM0CjU2 282 | N1dOU1doaXRlXE5TQ29sb3JTcGFjZUQwIDAAEAOADNIlJjk6V05TQ29sb3KiOSvSJSY8PVdOU0ltYWdl 283 | ojwrXxAPTlNLZXllZEFyY2hpdmVy0UBBVHJvb3SAAQAIABEAGgAjAC0AMgA3AEYATABXAF4AZQByAHkA 284 | gQCDAIUAigCMAI4AlQCaAKUApwCpAKsAsACzALUAtwC5ALsAwADXANkA2wlTCVgJYwlsCX8JgwmWCaEJ 285 | qgmvCbcJugm/Cc4J0gnZCeEJ7gnzCfUJ9wn8CgQKBwoMChQKFwopCiwKMQAAAAAAAAIBAAAAAAAAAEIA 286 | AAAAAAAAAAAAAAAAAAozA 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | -------------------------------------------------------------------------------- /Widget/SearchViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // SearchViewController.h 3 | // Today Scripts 4 | // 5 | // Created by Sam Rothenberg on 10/21/14. 6 | // Copyright (c) 2014 Sam Rothenberg. All rights reserved. 7 | // 8 | 9 | #import "TodayScript.h" 10 | 11 | @interface SearchViewController : NCWidgetSearchViewController 12 | 13 | @property TodayScript *script; 14 | 15 | @end 16 | 17 | 18 | @interface SearchProgramTextField : NSTextField 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /Widget/SearchViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // SearchViewController.m 3 | // Today Scripts 4 | // 5 | // Created by Sam Rothenberg on 10/21/14. 6 | // Copyright (c) 2014 Sam Rothenberg. All rights reserved. 7 | // 8 | 9 | #import "SearchViewController.h" 10 | #import 11 | 12 | @implementation SearchViewController 13 | { 14 | IBOutlet NSTextField *labelField; 15 | IBOutlet SearchProgramTextField *programField; 16 | IBOutlet NSTextView *scriptField; 17 | IBOutlet NSButton *autorunButton; 18 | IBOutlet NSButton *saveButton; 19 | 20 | TodayScript *_script; 21 | } 22 | 23 | - (TodayScript *)script { 24 | return _script; 25 | } 26 | - (void)setScript:(TodayScript *)script 27 | { 28 | // If we are given a script to edit, set up our fields to its values. 29 | if (script) { 30 | labelField.stringValue = script.label; 31 | programField.stringValue = script.program; 32 | scriptField.string = script.script; 33 | autorunButton.state = script.autoRun ? NSOnState : NSOffState; 34 | saveButton.stringValue = @"Save Script"; 35 | } 36 | // Otherwise, set up our fields with the default values. 37 | else { 38 | labelField.stringValue = @""; 39 | programField.stringValue = NSProcessInfo.processInfo.environment[@"SHELL"]; 40 | scriptField.string = @""; 41 | autorunButton.state = NSOnState; 42 | saveButton.stringValue = @"Add Script"; 43 | } 44 | 45 | [self willChangeValueForKey:@"script"]; 46 | _script = script; 47 | [self didChangeValueForKey:@"script"]; 48 | } 49 | 50 | - (void)viewDidLoad 51 | { 52 | [super viewDidLoad]; 53 | 54 | // Set up the appearence of the script field. 55 | scriptField.font = [NSFont fontWithName:@"Menlo" size:10]; 56 | scriptField.textColor = [NSColor colorWithWhite:1 alpha:1]; 57 | scriptField.backgroundColor = NSColor.clearColor; 58 | // Disable all substitutions in the script field. 59 | scriptField.automaticDashSubstitutionEnabled = NO; 60 | scriptField.automaticQuoteSubstitutionEnabled = NO; 61 | scriptField.automaticTextReplacementEnabled = NO; 62 | scriptField.automaticLinkDetectionEnabled = NO; 63 | scriptField.automaticSpellingCorrectionEnabled = NO; 64 | scriptField.automaticDataDetectionEnabled = NO; 65 | } 66 | 67 | // Method invoked when user presses the "Add Script" button. 68 | - (IBAction)resultSelected:(id)sender 69 | { 70 | // If the interpreter is not a valid executable file, style the text to 71 | // indicate the error to the user, then abort. 72 | NSString *programString = programField.stringValue; 73 | if (! [NSFileManager.defaultManager isExecutableFileAtPath:programString]) { 74 | programField.textColor = [NSColor colorWithRed:1.0 green:0.25 blue:0.25 alpha:1]; 75 | return; 76 | } 77 | 78 | // If we were not given an existing dictionary to modify, set that up to 79 | // work with. Otherwise, create a new one. 80 | TodayScript *script = self.script ?: [[TodayScript alloc] init]; 81 | 82 | script.program = programString; 83 | 84 | // Set the script to the dictionary if the user provided one. Otherwise, 85 | // remove any which may have previously existed. 86 | script.script = scriptField.string; 87 | 88 | // Set the script's title to the user provided one. 89 | script.label = labelField.stringValue; 90 | if (! script.label.length) 91 | // If they didn't provide a title, use the text of the script, or the 92 | // name of the program itself if there is no script. 93 | script.label = script.script.length 94 | ? script.script 95 | : script.program; 96 | 97 | // Set whether the script should run automatically according to the user's 98 | // specification. 99 | script.autoRun = (autorunButton.state != NSOffState); 100 | 101 | // Initialize the script's output and status to empty objects. 102 | script.output = @""; 103 | script.status = (id)NSNull.null; 104 | 105 | // Send the completed dictionary to the ListViewController. 106 | [self.delegate widgetSearch:self resultSelected:self.script]; 107 | } 108 | 109 | @end 110 | 111 | 112 | @implementation SearchProgramTextField 113 | 114 | // When the user starts editing the program field, make sure the text color goes 115 | // back to white if it was changed to indicate an error with the previous input. 116 | - (void)textDidBeginEditing:(NSNotification *)notification { 117 | self.textColor = [NSColor colorWithWhite:1 alpha:1]; 118 | } 119 | 120 | @end -------------------------------------------------------------------------------- /Widget/SearchViewController.xib: -------------------------------------------------------------------------------- 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 | 73 | 74 | 75 | 76 | 77 | 78 | 91 | 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 | -------------------------------------------------------------------------------- /Widget/TodayScript.h: -------------------------------------------------------------------------------- 1 | // 2 | // ScriptsArray.h 3 | // Today Scripts 4 | // 5 | // Created by Sam Rothenberg on 8/14/14. 6 | // Copyright (c) 2014 Sam Rothenberg. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | #import "TodayScripts.h" 13 | 14 | 15 | @interface TodayScriptArray : NSMutableArray 16 | 17 | + (TodayScriptArray *)sharedScripts; 18 | 19 | - (void)saveDefaults; 20 | 21 | - (NCUpdateResult)autoRun; 22 | 23 | @end 24 | 25 | 26 | @interface TodayScript : NSObject 27 | 28 | // The user-defined settings for the script. 29 | @property NSString *label; 30 | @property NSString *program; 31 | @property NSString *script; 32 | @property BOOL autoRun; 33 | @property BOOL showStatus; 34 | 35 | // The above properties, represented by a dictionary. The dictionary's keys are 36 | // TodayScriptLabelKey, TodayScriptProgramKey, etcetera. Setting this dictionary 37 | // also sets the corresponding properties. 38 | @property NSDictionary *dictionary; 39 | 40 | // Keep track of whether we are running or not. 41 | @property (readonly) BOOL running; 42 | 43 | // The exit status and TTY output of the script. Setting the value output string 44 | // also causes the attributed output string to update accordingly. 45 | @property (readonly) NSNumber *status; 46 | 47 | @property (readonly) NSString *output; 48 | @property (readonly) NSString *unescapedOutput; 49 | @property (readonly) NSAttributedString *attributedOutput; 50 | 51 | - (void)run; 52 | - (void)terminate; 53 | 54 | @end 55 | -------------------------------------------------------------------------------- /Widget/TodayScript.m: -------------------------------------------------------------------------------- 1 | // 2 | // ScriptsArray.m 3 | // Today Scripts 4 | // 5 | // Created by Sam Rothenberg on 8/14/14. 6 | // Copyright (c) 2014 Sam Rothenberg. All rights reserved. 7 | // 8 | 9 | #import "TodayScript.h" 10 | #import "AMR_ANSIEscapeHelper.h" 11 | 12 | // The queue we will use for asynchronous tasks. 13 | dispatch_queue_t queue; 14 | 15 | 16 | // The XPC connection. 17 | NSXPCConnection *XPC; 18 | // The XPC proxy object for the helper. 19 | id XPCHelper; 20 | // A lock for working with the XPC helper. 21 | NSConditionLock *XPCLock; 22 | 23 | 24 | // Translater from ANSI-escaped NSStrings to NSAttributedStrings. 25 | AMR_ANSIEscapeHelper *ANSIHelper; 26 | 27 | #define PADDING_LEFT 0 28 | #define COLUMN_WIDTH 6.02 29 | #define COLUMN_COUNT 40.0 30 | #define TAB_COLUMNS 8 31 | 32 | // Paragraph style describing our tab stops. 33 | NSMutableParagraphStyle *paragraphStyle; 34 | // Characters to be trimmed from the beginning and end of displayed output. 35 | NSCharacterSet *lineBreaks; 36 | 37 | 38 | @interface TodayScript () 39 | 40 | @property (readwrite) BOOL running; 41 | 42 | @property (readwrite) NSNumber *status; 43 | 44 | @property (readwrite) NSString *output; 45 | @property (readwrite) NSString *unescapedOutput; 46 | @property (readwrite) NSAttributedString *attributedOutput; 47 | 48 | @end 49 | @implementation TodayScript 50 | { 51 | NSString *UUID; 52 | } 53 | 54 | - (id)init 55 | { 56 | if (! (self = super.init)) return nil; 57 | 58 | // Generate a UUID for consistently identifying this script internally. 59 | UUID = NSUUID.UUID.UUIDString; 60 | 61 | // Initialize the result-related properties. 62 | self.status = (id)NSNull.null; 63 | self.output = @""; 64 | 65 | return self; 66 | } 67 | 68 | // When our dictionary version is requested, create one with the relevant items. 69 | - (NSDictionary *)dictionary 70 | { 71 | return @{ 72 | TodayScriptLabelKey : self.label, 73 | TodayScriptProgramKey : self.program, 74 | TodayScriptScriptKey : self.script, 75 | TodayScriptAutoRunKey : self.autoRun ? @YES : @NO, 76 | TodayScriptShowStatusKey : self.showStatus ? @YES : @NO 77 | }; 78 | } 79 | // When we are modeling our values from a dictionary, set the relevant items. 80 | - (void)setDictionary:(NSDictionary *)dictionary 81 | { 82 | self.label = dictionary[ TodayScriptLabelKey ]; 83 | self.program = dictionary[ TodayScriptProgramKey ]; 84 | self.script = dictionary[ TodayScriptScriptKey ]; 85 | self.autoRun = [dictionary[ TodayScriptAutoRunKey ] boolValue]; 86 | self.showStatus = [dictionary[ TodayScriptShowStatusKey ] boolValue]; 87 | // Inititalize our state-related properies as well. 88 | self.running = NO; 89 | self.status = (id)NSNull.null; 90 | self.output = @""; 91 | } 92 | 93 | - (void)runWithLock:(NSConditionLock *)lock resultPointer:(NSValue *)resultPointer 94 | { 95 | // If we were passed a pointer for tracking update results, upwrap it. 96 | NCUpdateResult *updateResult = resultPointer ? resultPointer.pointerValue : NULL; 97 | 98 | 99 | // Wait for our turn to influence the XPC connectivity. 100 | [XPCLock lock]; 101 | 102 | // Set to property to indicate that we are now running. 103 | self.running = YES; 104 | 105 | // Check the value of the lock. If it is 0, then the connection will not 106 | // currently be active, so we should awaken it. 107 | if (XPCLock.condition == 0) 108 | [XPC resume]; 109 | // Regardless of whether it was already active or whether we activated it, 110 | // increment the value of the lock to indicate our needing the connection. 111 | [XPCLock unlockWithCondition:XPCLock.condition + 1]; 112 | 113 | // Pass ourselves in dictionary form to the XPC helper, with handler block 114 | // for receiving the script's PID and initiating our observation of it. 115 | [XPCHelper launchScript:self.dictionary forUUID:UUID handler: ^(int status, NSString *output) 116 | { 117 | // Once the process has terminated, we are no longer in need of the 118 | // XPC connection. Wait for our turn again to influence it. 119 | [XPCLock lock]; 120 | 121 | // Check the value of the lock. If it is 1, then we were the last 122 | // ones to be using it, so it may now be suspended. 123 | if (XPCLock.condition == 1) 124 | [XPC suspend]; 125 | // Either way, decrement the lock's value to indicate our own lack of 126 | // need for it. 127 | [XPCLock unlockWithCondition:XPCLock.condition - 1]; 128 | 129 | // Unset our running status. Use the main thread so as to emit the 130 | // proper KVO and UI updates. 131 | [self performSelectorOnMainThread: 132 | @selector(setRunning:) withObject:nil waitUntilDone:NO]; 133 | 134 | // Create a variable representing whether we have updates to display. 135 | BOOL newData = NO; 136 | 137 | // If the script was terminated prematurely, we should set our status to 138 | // to a null object, and no updates will need to be reported. 139 | if (status < 0) 140 | [self performSelectorOnMainThread: 141 | @selector(setStatus:) withObject:NSNull.null waitUntilDone:NO]; 142 | 143 | // If the script ran to completion, process its results to see whether 144 | // they will need to be updated. 145 | else { 146 | // If we had not run to completion previously, or if the exit status 147 | // of the previous run was different than this one, our status is to 148 | // be updated with the new one. 149 | if (self.status == (id)NSNull.null || self.status.intValue != status) 150 | { 151 | [self performSelectorOnMainThread: 152 | @selector(setStatus:) withObject:@(status) waitUntilDone:NO]; 153 | 154 | newData = YES; 155 | } 156 | 157 | // Check whether the raw character output is identical to that of 158 | // the last completed run. 159 | if (! [self.output isEqualToString:output]) 160 | { 161 | // If the raw output has changed, update the property. 162 | self.output = output; 163 | 164 | // Parsed the ANSI escaped string to an attributed string. 165 | NSMutableAttributedString *attributedOutput = [ANSIHelper 166 | attributedStringWithANSIEscapedString:output].mutableCopy; 167 | 168 | // Get the bare characters from the parsed string. 169 | NSString *unescapedOutput = attributedOutput.string; 170 | 171 | // Get the unescaped output with excess line breaks trimmed off. 172 | NSString *trimmedOutput = [unescapedOutput stringByTrimmingCharactersInSet:lineBreaks]; 173 | 174 | // If the string is empty after being trimmed of line breaks, we 175 | // will not be displaying an output field, so keep it simple. 176 | if ([trimmedOutput isEqualToString:@""]) { 177 | attributedOutput = [[NSMutableAttributedString alloc] initWithString:@""]; 178 | unescapedOutput = @""; 179 | } 180 | 181 | // If the trimmed output is not empty, format the attributed and 182 | // unescaped versions of our output. The unescaped one is 183 | // inserted into a text field which is hidden, but expands with 184 | // its contents, causing the script's row view to expand with it. 185 | else { 186 | // Determine the range of the untrimmed output, and use it 187 | // to trim the line breaks from the attributed version. 188 | NSRange trimmedOutputRange = [unescapedOutput rangeOfString:trimmedOutput]; 189 | attributedOutput = [attributedOutput 190 | attributedSubstringFromRange:trimmedOutputRange].mutableCopy; 191 | // Apply our tab stops to the formatted string. 192 | 193 | [attributedOutput addAttribute:NSParagraphStyleAttributeName 194 | value:paragraphStyle range:NSMakeRange(0, attributedOutput.length)]; 195 | 196 | // This text field needs two extra line breaks to expand 197 | // the row properly. 198 | unescapedOutput = [@"\n\n" stringByAppendingString:trimmedOutput]; 199 | } 200 | 201 | // If the final attributed output does not match the one from 202 | // the previous run, we will need to update our output. 203 | if (! [attributedOutput isEqualToAttributedString:self.attributedOutput]) 204 | { 205 | [self performSelectorOnMainThread:@selector(setAttributedOutput:) 206 | withObject:attributedOutput waitUntilDone:NO]; 207 | 208 | [self performSelectorOnMainThread:@selector(setUnescapedOutput:) 209 | withObject:unescapedOutput waitUntilDone:NO]; 210 | 211 | newData = YES; 212 | } 213 | } 214 | } 215 | 216 | if (newData) { 217 | // If we were given a pointer to an update result variable, set it 218 | // to indicate that there are indeed changes. 219 | if (updateResult) 220 | *updateResult = NCUpdateResultNewData; 221 | 222 | // If we were passed a lock for tracking progress, we can now set it 223 | // to zero since we've already determined there are updates to show. 224 | if (lock) { 225 | [lock lock]; 226 | [lock unlockWithCondition:0]; 227 | } 228 | } 229 | // If we were passed a lock for tracking progress, we can now decrement 230 | // it to indicate we no longer need to be waited on. 231 | else if (lock) { 232 | [lock lock]; 233 | [lock unlockWithCondition:lock.condition - 1]; 234 | } 235 | }]; 236 | } 237 | 238 | - (void)run 239 | { 240 | // Safely determine whether we are already running. If we are not, go 241 | // through our run routine, no lock or result pointer required. 242 | [XPCLock lock]; 243 | BOOL running = self.running; 244 | [XPCLock unlock]; 245 | 246 | if (! running) 247 | [self runWithLock:nil resultPointer:nil]; 248 | } 249 | 250 | - (void)terminate 251 | { 252 | // Safely determine whether we are currently running. If in fact we are not, 253 | // ask the helper to terminate the process. 254 | [XPCLock lock]; 255 | BOOL running = self.running; 256 | [XPCLock unlock]; 257 | 258 | if (running) 259 | [XPCHelper terminateScriptForUUID:UUID]; 260 | } 261 | 262 | - (oneway void)dealloc { 263 | [self terminate]; 264 | } 265 | 266 | @end 267 | 268 | 269 | 270 | @implementation TodayScriptArray 271 | { 272 | // Backing store for our array. 273 | NSMutableArray *array; 274 | } 275 | 276 | + (TodayScriptArray *)sharedScripts 277 | { 278 | static TodayScriptArray *instance = nil; 279 | if (! instance) instance = [[TodayScriptArray alloc] init]; 280 | return instance; 281 | } 282 | 283 | - (id)init 284 | { 285 | if (! (self = super.init)) return nil; 286 | 287 | // We will use the default priority queue for asynchronous tasks. 288 | queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 289 | 290 | // Set up the XPC connection to the helper object. 291 | XPC = [NSXPCConnection.alloc initWithServiceName:@"org.samroth.Today-Scripts.Widget.XPC"]; 292 | XPC.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(XPCHelping)]; 293 | XPCHelper = XPC.remoteObjectProxy; 294 | XPCLock = [[NSConditionLock alloc] initWithCondition:0]; 295 | 296 | // Create the ANSI translator. 297 | ANSIHelper = [[AMR_ANSIEscapeHelper alloc] init]; 298 | 299 | // Define the font to use. 300 | ANSIHelper.font = [NSFont fontWithName:@"Menlo" size:10]; 301 | 302 | #define SET_AMR_SGRCode(CODE, RED, GREEN, BLUE, ALPHA) \ 303 | ANSIHelper.ansiColors[ @(AMR_SGRCode##CODE) ] = \ 304 | [NSColor colorWithRed:RED green:GREEN blue:BLUE alpha:ALPHA] 305 | 306 | // Define the standard text colors to use. 307 | SET_AMR_SGRCode( FgBlack, 0.31764706, 0.31764706, 0.31764706, 1.0 ); 308 | SET_AMR_SGRCode( FgRed, 0.94901961, 0.46666667, 0.47843137, 1.0 ); 309 | SET_AMR_SGRCode( FgGreen, 0.60000000, 0.80000000, 0.60000000, 1.0 ); 310 | SET_AMR_SGRCode( FgYellow, 1.00000000, 0.80000000, 0.40000000, 1.0 ); 311 | SET_AMR_SGRCode( FgBlue, 0.40000000, 0.60000000, 0.80000000, 1.0 ); 312 | SET_AMR_SGRCode( FgMagenta, 0.80000000, 0.60000000, 0.80000000, 1.0 ); 313 | SET_AMR_SGRCode( FgCyan, 0.40000000, 0.80000000, 0.80000000, 1.0 ); 314 | SET_AMR_SGRCode( FgWhite, 0.80000000, 0.80000000, 0.80000000, 1.0 ); 315 | // Background colors are the same as the text colors, but with half opacity. 316 | SET_AMR_SGRCode( BgBlack, 0.31764706, 0.31764706, 0.31764706, 0.5 ); 317 | SET_AMR_SGRCode( BgRed, 0.94901961, 0.46666667, 0.47843137, 0.5 ); 318 | SET_AMR_SGRCode( BgGreen, 0.60000000, 0.80000000, 0.60000000, 0.5 ); 319 | SET_AMR_SGRCode( BgYellow, 1.00000000, 0.80000000, 0.40000000, 0.5 ); 320 | SET_AMR_SGRCode( BgBlue, 0.40000000, 0.60000000, 0.80000000, 0.5 ); 321 | SET_AMR_SGRCode( BgMagenta, 0.80000000, 0.60000000, 0.80000000, 0.5 ); 322 | SET_AMR_SGRCode( BgCyan, 0.40000000, 0.80000000, 0.80000000, 0.5 ); 323 | SET_AMR_SGRCode( BgWhite, 0.80000000, 0.80000000, 0.80000000, 0.5 ); 324 | // Bright text colors are more vivid than normal text colors. 325 | SET_AMR_SGRCode( FgBrightBlack, 0.00000000, 0.00000000, 0.00000000, 1.0 ); 326 | SET_AMR_SGRCode( FgBrightRed, 0.86666667, 0.40000000, 0.40000000, 1.0 ); 327 | SET_AMR_SGRCode( FgBrightGreen, 0.72941176, 0.77254902, 0.36470588, 1.0 ); 328 | SET_AMR_SGRCode( FgBrightYellow, 0.92156863, 0.80392157, 0.38039216, 1.0 ); 329 | SET_AMR_SGRCode( FgBrightBlue, 0.55686275, 0.71764706, 0.87450980, 1.0 ); 330 | SET_AMR_SGRCode( FgBrightMagenta, 0.80784314, 0.67450980, 0.87058824, 1.0 ); 331 | SET_AMR_SGRCode( FgBrightCyan, 0.51372549, 0.79215686, 0.74901961, 1.0 ); 332 | SET_AMR_SGRCode( FgBrightWhite, 1.00000000, 1.00000000, 1.00000000, 1.0 ); 333 | // Bright backgrounds are the same as bright text, but with half opacity. 334 | SET_AMR_SGRCode( BgBrightBlack, 0.00000000, 0.00000000, 0.00000000, 0.5 ); 335 | SET_AMR_SGRCode( BgBrightRed, 0.86666667, 0.40000000, 0.40000000, 0.5 ); 336 | SET_AMR_SGRCode( BgBrightGreen, 0.72941176, 0.77254902, 0.36470588, 0.5 ); 337 | SET_AMR_SGRCode( BgBrightYellow, 0.92156863, 0.80392157, 0.38039216, 0.5 ); 338 | SET_AMR_SGRCode( BgBrightBlue, 0.55686275, 0.71764706, 0.87450980, 0.5 ); 339 | SET_AMR_SGRCode( BgBrightMagenta, 0.80784314, 0.67450980, 0.87058824, 0.5 ); 340 | SET_AMR_SGRCode( BgBrightCyan, 0.51372549, 0.79215686, 0.74901961, 0.5 ); 341 | SET_AMR_SGRCode( BgBrightWhite, 1.00000000, 1.00000000, 1.00000000, 0.5 ); 342 | 343 | // Default text color is our non-bright white. 344 | ANSIHelper.ansiColors[ @(AMR_SGRCodeFgReset) ] = ANSIHelper.defaultStringColor = 345 | ANSIHelper.ansiColors[ @(AMR_SGRCodeFgWhite) ]; 346 | 347 | // Default background is transparent. 348 | ANSIHelper.ansiColors[@( AMR_SGRCodeBgReset )] = NSColor.clearColor; 349 | 350 | paragraphStyle = [[NSMutableParagraphStyle alloc] init]; 351 | paragraphStyle.defaultTabInterval = COLUMN_WIDTH * TAB_COLUMNS; 352 | 353 | NSMutableArray *tabStops = [[NSMutableArray alloc] init]; 354 | for (NSUInteger tabIndex = 1; tabIndex < COLUMN_COUNT / TAB_COLUMNS; tabIndex++) 355 | { 356 | CGFloat tabLocation = COLUMN_WIDTH * TAB_COLUMNS * tabIndex + PADDING_LEFT; 357 | [tabStops addObject:[NSTextTab.alloc initWithType:NSLeftTabStopType location:tabLocation]]; 358 | } 359 | paragraphStyle.tabStops = tabStops; 360 | 361 | // Set up our list of line breaks. 362 | lineBreaks = [NSCharacterSet characterSetWithCharactersInString:@"\n\r"]; 363 | 364 | 365 | 366 | // Set up the "true" backing array for our live scripts list. 367 | array = [[NSMutableArray alloc] init]; 368 | 369 | // [NSUserDefaults.standardUserDefaults removeObjectForKey:@"Scripts"]; 370 | 371 | // Get the list of scripts as stored in our defaults. 372 | NSArray *defaultsScripts = [NSUserDefaults.standardUserDefaults arrayForKey:@"Scripts"]; 373 | 374 | // Iterate through the scripts to translate them to our live representation. 375 | for (id defaultsScript in defaultsScripts) 376 | { 377 | // If the script is not dictionary form, ignore it. 378 | if (! [defaultsScript isKindOfClass:NSDictionary.class]) 379 | continue; 380 | 381 | TodayScript *script = [[TodayScript alloc] init]; 382 | script.dictionary = defaultsScript; 383 | [array addObject:script]; 384 | } 385 | 386 | return self; 387 | } 388 | 389 | - (NSUInteger)count { 390 | return array.count; 391 | } 392 | - (id)objectAtIndex:(NSUInteger)index { 393 | return [array objectAtIndex:index]; 394 | } 395 | 396 | // All changes to our array should be saved to the array in our user defaults. 397 | // This method does this for each overridden NSMutableArray method. 398 | - (void)saveDefaults 399 | { 400 | // Create a new array to be copied to our defaults. 401 | NSMutableArray *defaultsScripts = [[NSMutableArray alloc] init]; 402 | // Iterate through our own script dictionaries in order to add them to it. 403 | for (TodayScript *script in array) 404 | [defaultsScripts addObject:script.dictionary]; 405 | 406 | // Finally, write the array to our defaults. 407 | [NSUserDefaults.standardUserDefaults setObject:defaultsScripts forKey:@"Scripts"]; 408 | } 409 | - (void)insertObject:(id)anObject atIndex:(NSUInteger)index { 410 | [array insertObject:anObject atIndex:index]; 411 | [self saveDefaults]; 412 | } 413 | - (void)removeObjectAtIndex:(NSUInteger)index { 414 | [array removeObjectAtIndex:index]; 415 | [self saveDefaults]; 416 | } 417 | - (void)addObject:(id)anObject { 418 | [array addObject:anObject]; 419 | [self saveDefaults]; 420 | } 421 | - (void)removeLastObject { 422 | [array removeLastObject]; 423 | [self saveDefaults]; 424 | } 425 | - (void)replaceObjectAtIndex:(NSUInteger)index withObject:(id)anObject { 426 | [array replaceObjectAtIndex:index withObject:anObject]; 427 | [self saveDefaults]; 428 | } 429 | 430 | - (NCUpdateResult)autoRun 431 | { 432 | // Count how many scripts we will need to run. 433 | NSUInteger count = 0; 434 | for (TodayScript *script in array) 435 | if (script.autoRun) 436 | count++; 437 | 438 | // If there are no scripts to run, stop here, and return an indication that 439 | // there are no changes to display. 440 | if (! count) 441 | return NCUpdateResultNoData; 442 | 443 | // Initialize a lock to use for waiting until each script has decremented it 444 | // after their completion. 445 | NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:count]; 446 | 447 | // Create a variable that's modifiable by blocks, to use for tracking 448 | // whether we will have updates to display in the end. 449 | __block NCUpdateResult updateResult = NCUpdateResultNoData; 450 | NSValue *resultPointer = [NSValue valueWithPointer:&updateResult]; 451 | 452 | // Run each script that is marked to automatically run. 453 | for (TodayScript *script in array) 454 | if (script.autoRun) 455 | [script runWithLock:lock resultPointer:resultPointer]; 456 | 457 | // Set up a timeout routine to dispatch after three seconds. 458 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC), queue, ^ 459 | { 460 | // If we time out, we will tell Notification Center to update the view 461 | // in order to display the ongoing progress to the user. 462 | updateResult = NCUpdateResultNewData; 463 | // Set the value of lock to zero in order to allow us to proceed. 464 | [lock lock]; 465 | [lock unlockWithCondition:0]; 466 | }); 467 | 468 | // Wait on our lock until all of the scripts have decremented it, or until 469 | // one has updates to display and sets it to zero, or until we've timed out. 470 | [lock lockWhenCondition:0]; 471 | [lock unlock]; 472 | 473 | // Return the final results to Notification Center. 474 | return updateResult; 475 | } 476 | 477 | @end 478 | -------------------------------------------------------------------------------- /Widget/TodayViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // TodayViewController.h 3 | // Scripts 4 | // 5 | // Created by Sam Rothenberg on 8/14/14. 6 | // Copyright (c) 2014 Sam Rothenberg. All rights reserved. 7 | // 8 | 9 | #import "TodayScript.h" 10 | #import "EditViewController.h" 11 | 12 | 13 | @interface TodayViewController : NSViewController 14 | 15 | @property (strong) IBOutlet NCWidgetListViewController *listViewController; 16 | @property (strong) EditViewController *editViewController; 17 | @property (strong) NSArrayController *arrayController; 18 | 19 | @end 20 | 21 | extern TodayViewController *todayViewController; 22 | -------------------------------------------------------------------------------- /Widget/TodayViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // TodayViewController.m 3 | // Scripts 4 | // 5 | // Created by Sam Rothenberg on 8/14/14. 6 | // Copyright (c) 2014 Sam Rothenberg. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "TodayScript.h" 12 | #import "TodayViewController.h" 13 | #import "EditViewController.h" 14 | #import "ListRowViewController.h" 15 | 16 | 17 | TodayViewController *todayViewController; 18 | 19 | 20 | #pragma mark - Widget Implementation 21 | 22 | 23 | @implementation TodayViewController 24 | 25 | - (void)viewDidLoad 26 | { 27 | [super viewDidLoad]; 28 | 29 | todayViewController = self; 30 | 31 | // Set up the view controller for adding and editing scripts. 32 | self.editViewController = [[EditViewController alloc] init]; 33 | 34 | // Set up our array controller to manage the array which coordiates with our 35 | // persistent user defaults. 36 | self.arrayController = [NSArrayController.alloc initWithContent:TodayScriptArray.sharedScripts]; 37 | 38 | // If our user defaults contain no scripts, set up an introductory one. 39 | if (! TodayScriptArray.sharedScripts.count) 40 | { 41 | TodayScript *script = [[TodayScript alloc] init]; 42 | script.label = @"Welcome"; 43 | script.program = @"/bin/sh"; 44 | script.script = @"echo 'Click the Info button above to start adding scripts.'"; 45 | script.autoRun = YES; 46 | script.showStatus = YES; 47 | [self.arrayController addObject:script]; 48 | } 49 | 50 | // Set up the widget list view controller to get its content from our array 51 | // controller. 52 | self.listViewController.contents = self.arrayController.arrangedObjects; 53 | } 54 | 55 | 56 | 57 | #pragma mark - Getting and Displaying Updates 58 | 59 | // Notification Center calls this method to give us an opportunity to provide 60 | // updates. Refresh the widget's contents in preparation for a snapshot. 61 | - (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult result))handler 62 | { 63 | // Tell the script array to auto run the scripts as necessary, and return 64 | // the update status it indicates. 65 | handler(TodayScriptArray.sharedScripts.autoRun); 66 | } 67 | 68 | - (NSEdgeInsets)widgetMarginInsetsForProposedMarginInsets:(NSEdgeInsets)defaultMarginInset { 69 | // Override the left margin so that the list view is flush with the edge. 70 | defaultMarginInset.left = 0; 71 | defaultMarginInset.right = 0; 72 | return defaultMarginInset; 73 | } 74 | 75 | 76 | 77 | #pragma mark - Editing the List of Scripts 78 | 79 | - (NSViewController *)widgetList:(NCWidgetListViewController *)list viewControllerForRow:(NSUInteger)row 80 | { 81 | // Return a new view controller subclass for displaying an item of widget 82 | // content. The NCWidgetListViewController will set the representedObject 83 | // of this view controller to one of the objects in its contents array. 84 | return [[ListRowViewController alloc] init]; 85 | } 86 | 87 | - (BOOL)widgetList:(NCWidgetListViewController *)list shouldReorderRow:(NSUInteger)row { 88 | // Return YES to allow the item to be reordered in the list by the user. 89 | return YES; 90 | } 91 | 92 | - (void)widgetList:(NCWidgetListViewController *)list didReorderRow:(NSUInteger)row toRow:(NSUInteger)newIndex { 93 | // The user has reordered an item in the list. 94 | TodayScript *script = [self.arrayController.arrangedObjects objectAtIndex:row]; 95 | [self.arrayController removeObjectAtArrangedObjectIndex:row]; 96 | [self.arrayController insertObject:script atArrangedObjectIndex:newIndex]; 97 | } 98 | 99 | - (BOOL)widgetList:(NCWidgetListViewController *)list shouldRemoveRow:(NSUInteger)row { 100 | // Return YES to allow the item to be removed from the list by the user. 101 | return YES; 102 | } 103 | 104 | - (void)widgetList:(NCWidgetListViewController *)list didRemoveRow:(NSUInteger)row { 105 | // The user has removed an item from the list. 106 | [self.arrayController removeObjectAtArrangedObjectIndex:row]; 107 | } 108 | 109 | - (void)widgetListPerformAddAction:(NCWidgetListViewController *)list 110 | { 111 | // The user has clicked the add button in the list view. 112 | [self.editViewController createScript]; 113 | } 114 | 115 | - (BOOL)widgetAllowsEditing { 116 | return YES; 117 | } 118 | 119 | - (void)widgetDidBeginEditing { 120 | // The user has clicked the edit button. Put the list view into edit mode. 121 | self.listViewController.editing = YES; 122 | } 123 | 124 | - (void)widgetDidEndEditing { 125 | // The user has clicked the Done button, begun editing another widget, or NC 126 | // has been closed. Take the list view out of editing mode. 127 | self.listViewController.contents = self.arrayController.arrangedObjects; 128 | self.listViewController.editing = NO; 129 | } 130 | 131 | @end 132 | -------------------------------------------------------------------------------- /Widget/TodayViewController.xib: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /Widget/Widget.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Widget/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Display name and description for this extension. */ 2 | "CFBundleDisplayName" = "Scripts"; 3 | "com.apple.notificationcenter.widget.description" = "Observe the results of scripts run when checking Notification Center"; 4 | 5 | -------------------------------------------------------------------------------- /XPC/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | XPC 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | org.samroth.Today-Scripts.Widget.$(PRODUCT_NAME:rfc1034identifier) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | XPC! 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | NSHumanReadableCopyright 26 | Copyright © 2014 Sam Rothenberg. All rights reserved. 27 | XPCService 28 | 29 | ServiceType 30 | Application 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /XPC/XPCHelper.m: -------------------------------------------------------------------------------- 1 | // 2 | // XPCHelper.m 3 | // xpc 4 | // 5 | // Created by Sam Rothenberg on 8/10/14. 6 | // Copyright (c) 2014 Sam Rothenberg. All rights reserved. 7 | // 8 | 9 | #import "TodayScripts.h" 10 | #import "XPCMain.h" 11 | #include 12 | 13 | 14 | @implementation XPCHelper 15 | { 16 | // The dictionary of environment variables for spawned tasks. 17 | NSMutableDictionary *environment; 18 | // Queue for performing asynchonous tasks. 19 | dispatch_queue_t queue; 20 | 21 | // A dictionary of the active tasks. 22 | NSMutableDictionary *tasks; 23 | } 24 | 25 | - (id)init 26 | { 27 | if (! (self = super.init)) return nil; 28 | 29 | // Create an environment for tasks based on our own, and add the details of 30 | // our "terminal." 31 | environment = NSProcessInfo.processInfo.environment.mutableCopy; 32 | environment[@"TERM" ] = @"ansi"; 33 | environment[@"COLUMNS"] = @"40"; 34 | environment[@"PAGER" ] = @"/bin/cat"; 35 | 36 | // We will perform asynchronous tasks on the default background queue. 37 | queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 38 | 39 | // Set up the dictionary to keep records of our tasks. 40 | tasks = [[NSMutableDictionary alloc] init]; 41 | 42 | return self; 43 | } 44 | 45 | - (void)launchScript:(NSDictionary *)script forUUID:(NSString *)UUID handler:(XPCHandler)handler; 46 | { 47 | // NSURL *programURL = [NSURL URLWithString:script[TodayScriptProgramKey]]; 48 | // NSUserUnixTask *task = [[NSUserUnixTask alloc] initWithURL:programURL error:nil]; 49 | 50 | // Create a new task object and add it to our dictionary. 51 | NSTask *task = [[NSTask alloc] init]; 52 | tasks[UUID] = task; 53 | 54 | // Set the task's program to the user's shell. 55 | task.launchPath = script[TodayScriptProgramKey]; 56 | // Set the task's environment as previously determined. 57 | task.environment = environment; 58 | // Set our current directory to the user's home directory. 59 | task.currentDirectoryPath = environment[@"HOME"]; 60 | 61 | // Open a 40-character wide pseudo-TTY for this script, getting its file 62 | // descriptors. 63 | int master, slave; 64 | struct winsize size = { .ws_col = 40 }; 65 | openpty(&master, &slave, NULL, NULL, &size); 66 | 67 | // Set up handles for the TTY's file descriptors, such that they close them 68 | // once we're done. 69 | NSFileHandle *masterHandle = NSFileHandle.alloc, *slaveHandle = NSFileHandle.alloc; 70 | masterHandle = [masterHandle initWithFileDescriptor:master closeOnDealloc:YES]; 71 | slaveHandle = [slaveHandle initWithFileDescriptor:slave closeOnDealloc:YES]; 72 | 73 | // The script's output will be sent to the pseudo-TTY. 74 | task.standardOutput = task.standardError = slaveHandle; 75 | 76 | // If we were provided a script, convert it to UTF-8 to pass to the program. 77 | NSString *scriptString = script[TodayScriptScriptKey]; 78 | if (scriptString.length) { 79 | // We will be piping the script to the program via its standard input. 80 | // Write the script to the pipe so that it is ready for the interpreter. 81 | NSPipe *pipe = NSPipe.pipe; 82 | task.standardInput = pipe.fileHandleForReading; 83 | [pipe.fileHandleForWriting writeData:[scriptString dataUsingEncoding:NSUTF8StringEncoding]]; 84 | [pipe.fileHandleForWriting closeFile]; 85 | } 86 | 87 | // Create a data object which can be modified by the TTY reader block, as 88 | // well as a semaphore which it can use to signal the termination block. 89 | __block NSData *outputData = nil; 90 | dispatch_semaphore_t outputSemaphore = dispatch_semaphore_create(0); 91 | 92 | dispatch_async(queue, ^ 93 | { 94 | // Read the master handle in the background until the TTY is closed. 95 | outputData = [masterHandle readDataToEndOfFile]; 96 | // After we've set the data object, we may signal the termination block 97 | // that it is ready. 98 | dispatch_semaphore_signal(outputSemaphore); 99 | }); 100 | 101 | task.terminationHandler = ^(NSTask *task) 102 | { 103 | // If the task did not complete on its own volition, we we be returning 104 | // an invalid exit status and output. 105 | NSString *output = nil; 106 | int status = -1; 107 | 108 | // Close the TTY such that the reader thread stops waiting on it. 109 | [slaveHandle closeFile]; 110 | 111 | // If the task ran to completion on its own volition, we will need to 112 | // process its results. 113 | if (task.terminationReason == NSTaskTerminationReasonExit) 114 | { 115 | // Wait for the reader thread to set the data object and signal us. 116 | dispatch_semaphore_wait(outputSemaphore, DISPATCH_TIME_FOREVER); 117 | // We will return the actual exit status of the process. 118 | status = task.terminationStatus; 119 | // Convert the UTF-8 output from the reader block to a string. 120 | output = [[NSString alloc] initWithData:outputData encoding:NSUTF8StringEncoding]; 121 | } 122 | 123 | // Return the results to the widget. 124 | handler(status, output); 125 | 126 | // Remove the task from our records. 127 | [tasks removeObjectForKey:UUID]; 128 | }; 129 | 130 | // Launch the task. 131 | [task launch]; 132 | } 133 | 134 | - (void)terminateScriptForUUID:(NSString *)UUID { 135 | [(NSTask *)tasks[UUID] terminate]; 136 | } 137 | 138 | @end 139 | -------------------------------------------------------------------------------- /XPC/XPCMain.h: -------------------------------------------------------------------------------- 1 | // 2 | // XPCMain.h 3 | // Today Scripts 4 | // 5 | // Created by Sam Rothenberg on 8/14/14. 6 | // Copyright (c) 2014 Sam Rothenberg. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "TodayScripts.h" 11 | 12 | @interface XPCListenerDelegate : NSObject @end 13 | XPCListenerDelegate *listenerDelegate; 14 | 15 | NSXPCListener *listener; 16 | NSXPCConnection *connection; 17 | 18 | // This object implements the protocol which we have defined. It provides the 19 | // actual behavior for the service. It is 'exported' by the service to make it 20 | // available to the process hosting the service over an NSXPCConnection. 21 | @interface XPCHelper : NSObject @end 22 | XPCHelper *helper; 23 | -------------------------------------------------------------------------------- /XPC/XPCMain.m: -------------------------------------------------------------------------------- 1 | // 2 | // XPCMain.m 3 | // xpc 4 | // 5 | // Created by Sam Rothenberg on 8/10/14. 6 | // Copyright (c) 2014 Sam Rothenberg. All rights reserved. 7 | // 8 | 9 | #import "XPCMain.h" 10 | 11 | @implementation XPCListenerDelegate 12 | 13 | - (BOOL)listener:(NSXPCListener *)aListener shouldAcceptNewConnection:(NSXPCConnection *)aConnection 14 | { 15 | helper = [XPCHelper.alloc init]; 16 | 17 | // This method is where the NSXPCListener configures, accepts, and resumes a new incoming NSXPCConnection. 18 | connection = aConnection; 19 | 20 | // Configure the connection. 21 | // First, set the interface that the exported object implements. 22 | connection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(XPCHelping)]; 23 | 24 | // Next, set the object that the connection exports. All messages sent on the connection to this service will be sent to the exported object to handle. The connection retains the exported object. 25 | connection.exportedObject = helper; 26 | 27 | // Resuming the connection allows the system to deliver more incoming messages. 28 | [connection resume]; 29 | 30 | // Returning YES from this method tells the system that you have accepted this connection. If you want to reject the connection for some reason, call -invalidate on the connection and return NO. 31 | return YES; 32 | } 33 | 34 | @end 35 | 36 | int main() 37 | { 38 | // Set up the one NSXPCListener for this service. It will handle all incoming connections. 39 | listener = NSXPCListener.serviceListener; 40 | 41 | // Create the delegate for the service. 42 | listenerDelegate = [XPCListenerDelegate.alloc init]; 43 | listener.delegate = listenerDelegate; 44 | 45 | // Resuming the serviceListener starts this service. This method does not return. 46 | [listener resume]; 47 | 48 | return 0; 49 | } 50 | --------------------------------------------------------------------------------