diff --git a/API.md b/API.md index 39bcf851..4fc3acc3 100644 --- a/API.md +++ b/API.md @@ -1,17 +1,7 @@ JavaScript API ============== -This documentation is an overview of the JavaScript API provided by Phoenix. Use this as a guide for writing your window management script. Your script should reside in `~/.phoenix.js`. Phoenix includes [Underscore.js](http://underscorejs.org) (1.8.3) — you can use its features in your configuration. Underscore provides useful helpers for handling JavaScript functions and objects. - -You may add JavaScript pre-processing to your `~/.phoenix.js` file by adding a [Shebang](https://en.wikipedia.org/wiki/Shebang_(Unix)) to the beginning of your file. For example, use [Babel](https://babeljs.io/) to pre-process ES2015 JavaScript syntax: -```javascript -#!/usr/bin/env babel -const handlers = []; -handlers.push(Phoenix.bind('c', ['alt', 'shift'], () => { - const app = App.launch('Google Chrome'); - app.focus(); -})); -``` +This documentation is an overview of the JavaScript API provided by Phoenix. Use this as a guide for writing your window management script. Your script should reside in `~/.phoenix.js`. Phoenix includes [Underscore.js](http://underscorejs.org) (1.8.3) — you can use its features in your configuration. Underscore provides useful helpers for handling JavaScript functions and objects. You may also use JavaScript [preprocessing](#preprocessing) and use languages such as CoffeeScript to write your Phoenix-configuration. ## API @@ -67,6 +57,33 @@ var handler = Phoenix.on('screensDidChange', function () {}); Your configuration file is loaded when the app launches. All functions are evaluated (and executed if necessary) when this happens. Phoenix also reloads the configuration when any changes are detected to the file. You may also reload the configuration manually from the status bar or programmatically from your script. +## Preprocessing + +You may add JavaScript preprocessing to your configuration by adding a [Shebang](https://en.wikipedia.org/wiki/Shebang_(Unix))-directive to the beginning of your file. For example, use [CoffeeScript](http://coffeescript.org) to write your configuration: + +```coffeescript +#!/usr/bin/env coffee + +keys = [] + +keys.push Phoenix.bind 's', [ 'ctrl', 'shift' ], -> + + App.launch('Safari').focus() +``` + +Or use [Babel](http://babeljs.io) to use ECMAScript 6 JavaScript: + +```javascript +#!/usr/bin/env babel + +const keys = []; + +keys.push(Phoenix.bind('s', [ 'ctrl', 'shift' ], () => { + + App.launch('Safari').focus(); +})); +``` + ## 1. Keys All valid keys for binding are as follows: diff --git a/CHANGELOG.md b/CHANGELOG.md index 36750f26..c251032c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Release: dd.mm.yyyy ### New - Phoenix now supports events! See the [API](API.md#2-events). To bind an event to a callback function, you call the `on`-function for the `Phoenix`-object. +- You may now use JavaScript [preprocessing](API.md#preprocessing) and use languages such as CoffeeScript to write your Phoenix-configuration (#45). Thanks @shayne! ### Changes diff --git a/Phoenix.xcodeproj/project.pbxproj b/Phoenix.xcodeproj/project.pbxproj index 987d2f53..be804258 100644 --- a/Phoenix.xcodeproj/project.pbxproj +++ b/Phoenix.xcodeproj/project.pbxproj @@ -7,16 +7,16 @@ objects = { /* Begin PBXBuildFile section */ - 1C93D6641BE11FF100649405 /* PHScriptHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C93D6631BE11FF100649405 /* PHScriptHelper.m */; settings = {ASSET_TAGS = (); }; }; + 1C93D6641BE11FF100649405 /* PHShebangPreprocessor.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C93D6631BE11FF100649405 /* PHShebangPreprocessor.m */; }; A7183B671B6E865F00842E13 /* PHKeyHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = A7183B661B6E865F00842E13 /* PHKeyHandler.m */; }; A72EAD041B5CE34800DD537B /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = A72EAD031B5CE34800DD537B /* Credits.rtf */; }; A72EAD071B5D00B800DD537B /* PHModalWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = A72EAD061B5D00B800DD537B /* PHModalWindowController.m */; }; A72ED7B61B6E25940064E35B /* PHPhoenix.m in Sources */ = {isa = PBXBuildFile; fileRef = A72ED7B51B6E25940064E35B /* PHPhoenix.m */; }; A73C2CA81B9078A30004C663 /* PHCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = A73C2CA71B9078A30004C663 /* PHCommand.m */; }; A73C2CAB1B9088040004C663 /* PHNotificationHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = A73C2CAA1B9088040004C663 /* PHNotificationHelper.m */; }; - A741336C1BB7EEAC008DAF39 /* PHHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = A741336B1BB7EEAC008DAF39 /* PHHandler.m */; settings = {ASSET_TAGS = (); }; }; - A741336F1BB7F228008DAF39 /* PHEventHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = A741336E1BB7F228008DAF39 /* PHEventHandler.m */; settings = {ASSET_TAGS = (); }; }; - A74133721BB7F556008DAF39 /* PHEventTranslator.m in Sources */ = {isa = PBXBuildFile; fileRef = A74133711BB7F556008DAF39 /* PHEventTranslator.m */; settings = {ASSET_TAGS = (); }; }; + A741336C1BB7EEAC008DAF39 /* PHHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = A741336B1BB7EEAC008DAF39 /* PHHandler.m */; }; + A741336F1BB7F228008DAF39 /* PHEventHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = A741336E1BB7F228008DAF39 /* PHEventHandler.m */; }; + A74133721BB7F556008DAF39 /* PHEventTranslator.m in Sources */ = {isa = PBXBuildFile; fileRef = A74133711BB7F556008DAF39 /* PHEventTranslator.m */; }; A762478A1B81FD3C00ECF209 /* PHAXUIElement.m in Sources */ = {isa = PBXBuildFile; fileRef = A76247891B81FD3C00ECF209 /* PHAXUIElement.m */; }; A79B89E01B5E74C1004DDE82 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = A79B89DE1B5E74C1004DDE82 /* MainMenu.xib */; }; A79B89E21B5E75CA004DDE82 /* ModalWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = A79B89E11B5E75CA004DDE82 /* ModalWindow.xib */; }; @@ -34,8 +34,8 @@ A79C46AA1B5BF41900C460CF /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A79C46A91B5BF41900C460CF /* Images.xcassets */; }; A7D1D9A91B6FD6A300E00CC9 /* PHKeyTranslator.m in Sources */ = {isa = PBXBuildFile; fileRef = A7D1D9A81B6FD6A300E00CC9 /* PHKeyTranslator.m */; }; A7D1F7651B5BFF5B0052E646 /* PhoenixTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A7D1F7641B5BFF5B0052E646 /* PhoenixTests.m */; }; - A7E6DF6C1BBAB5B8001920B4 /* PHAXObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = A7E6DF6B1BBAB5B8001920B4 /* PHAXObserver.m */; settings = {ASSET_TAGS = (); }; }; - A7E6DF6F1BBAC1F3001920B4 /* PHAccessibilityObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = A7E6DF6E1BBAC1F3001920B4 /* PHAccessibilityObserver.m */; settings = {ASSET_TAGS = (); }; }; + A7E6DF6C1BBAB5B8001920B4 /* PHAXObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = A7E6DF6B1BBAB5B8001920B4 /* PHAXObserver.m */; }; + A7E6DF6F1BBAC1F3001920B4 /* PHAccessibilityObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = A7E6DF6E1BBAC1F3001920B4 /* PHAccessibilityObserver.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -49,8 +49,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 1C93D6621BE11FF100649405 /* PHScriptHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PHScriptHelper.h; sourceTree = ""; }; - 1C93D6631BE11FF100649405 /* PHScriptHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PHScriptHelper.m; sourceTree = ""; }; + 1C93D6621BE11FF100649405 /* PHShebangPreprocessor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PHShebangPreprocessor.h; sourceTree = ""; }; + 1C93D6631BE11FF100649405 /* PHShebangPreprocessor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PHShebangPreprocessor.m; sourceTree = ""; }; A7183B651B6E865F00842E13 /* PHKeyHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PHKeyHandler.h; sourceTree = ""; }; A7183B661B6E865F00842E13 /* PHKeyHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PHKeyHandler.m; sourceTree = ""; }; A72A6B031B934C2000F2C4EF /* PHIdentifiableJSExport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PHIdentifiableJSExport.h; sourceTree = ""; }; @@ -106,6 +106,7 @@ A7E6DF6B1BBAB5B8001920B4 /* PHAXObserver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PHAXObserver.m; sourceTree = ""; }; A7E6DF6D1BBAC1F3001920B4 /* PHAccessibilityObserver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PHAccessibilityObserver.h; sourceTree = ""; }; A7E6DF6E1BBAC1F3001920B4 /* PHAccessibilityObserver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PHAccessibilityObserver.m; sourceTree = ""; }; + A7EBD1C21BFDF8CE000D8E5F /* PHPreprocessor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PHPreprocessor.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -155,8 +156,6 @@ A79C468B1B5BF3C100C460CF /* PHOpenAtLoginHelper.m */, A79C468E1B5BF3C100C460CF /* PHUniversalAccessHelper.h */, A79C468F1B5BF3C100C460CF /* PHUniversalAccessHelper.m */, - 1C93D6621BE11FF100649405 /* PHScriptHelper.h */, - 1C93D6631BE11FF100649405 /* PHScriptHelper.m */, ); name = Helpers; sourceTree = ""; @@ -260,6 +259,7 @@ A77952C61BBE7E0300FC8D38 /* Observers */, A79C46801B5BF3C100C460CF /* PHAppDelegate.h */, A79C46811B5BF3C100C460CF /* PHAppDelegate.m */, + A7EBD1C11BFDF7E1000D8E5F /* Preprocessors */, A79C465A1B5BF31100C460CF /* Supporting Files */, A73FF10C1BBD8F350024CF97 /* Translators */, ); @@ -294,6 +294,16 @@ name = "Supporting Files"; sourceTree = ""; }; + A7EBD1C11BFDF7E1000D8E5F /* Preprocessors */ = { + isa = PBXGroup; + children = ( + A7EBD1C21BFDF8CE000D8E5F /* PHPreprocessor.h */, + 1C93D6621BE11FF100649405 /* PHShebangPreprocessor.h */, + 1C93D6631BE11FF100649405 /* PHShebangPreprocessor.m */, + ); + name = Preprocessors; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -410,7 +420,7 @@ A74133721BB7F556008DAF39 /* PHEventTranslator.m in Sources */, A7E6DF6F1BBAC1F3001920B4 /* PHAccessibilityObserver.m in Sources */, A79C46971B5BF3C100C460CF /* PHApp.m in Sources */, - 1C93D6641BE11FF100649405 /* PHScriptHelper.m in Sources */, + 1C93D6641BE11FF100649405 /* PHShebangPreprocessor.m in Sources */, A7D1D9A91B6FD6A300E00CC9 /* PHKeyTranslator.m in Sources */, A79C46A01B5BF3C100C460CF /* PHWindow.m in Sources */, A72EAD071B5D00B800DD537B /* PHModalWindowController.m in Sources */, diff --git a/Phoenix/PHContext.m b/Phoenix/PHContext.m index 568168d3..615ca3fe 100644 --- a/Phoenix/PHContext.m +++ b/Phoenix/PHContext.m @@ -11,9 +11,9 @@ #import "PHModalWindowController.h" #import "PHMouse.h" #import "PHNotificationHelper.h" -#import "PHScriptHelper.h" #import "PHPathWatcher.h" #import "PHPhoenix.h" +#import "PHShebangPreprocessor.h" #import "PHWindow.h" @interface PHContext () @@ -119,17 +119,15 @@ - (void) loadScript:(NSString *)path { NSString *script = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error]; if (error) { - NSString *message = [NSString stringWithFormat: - @"Error: Could not read file in path “%@” to string. (%@)", path, error]; - [self handleException:message]; + NSLog(@"Error: Could not read file in path “%@” to string. (%@)", path, error); } - NSString *preprocessError; - script = [PHScriptHelper preprocessScriptIfNeeded:script atPath:path errorMessage:&preprocessError]; + NSError *preprocessError; + script = [PHShebangPreprocessor process:script atPath:path error:&preprocessError]; if (preprocessError) { - NSString *message = [NSString stringWithFormat:@"Error: Preprocess failed with error: %@", preprocessError]; - [self handleException:message]; + [self handleException:[NSString stringWithFormat:@"Preprocessing failed. (%@)", + preprocessError.localizedDescription]]; } [self.context evaluateScript:script]; diff --git a/Phoenix/PHPreprocessor.h b/Phoenix/PHPreprocessor.h new file mode 100644 index 00000000..be6916e2 --- /dev/null +++ b/Phoenix/PHPreprocessor.h @@ -0,0 +1,13 @@ +/* + * Phoenix is released under the MIT License. Refer to https://github.com/kasper/phoenix/blob/master/LICENSE.md + */ + +@import Foundation; + +@protocol PHPreprocessor + +#pragma mark - Preprocessing + ++ (NSString *) process:(NSString *)script atPath:(NSString *)path error:(NSError **)error; + +@end diff --git a/Phoenix/PHShebangPreprocessor.h b/Phoenix/PHShebangPreprocessor.h index 2f82041c..9fb37637 100644 --- a/Phoenix/PHShebangPreprocessor.h +++ b/Phoenix/PHShebangPreprocessor.h @@ -4,10 +4,11 @@ @import Foundation; -@interface PHShebangPreprocessor : NSObject +#import "PHPreprocessor.h" -#pragma mark - Script Preprocessing +static NSString * const PHShebangPreprocessorErrorDomain = @"PHShebangPreprocessorErrorDomain"; +static const NSInteger PHShebangPreprocessorErrorCode = -1; -+ (nullable NSString *)preprocessScriptIfNeeded:(nonnull NSString *)script atPath:(NSString * __nonnull)path errorMessage:(NSString * _Nullable * _Nonnull)errorMessage; +@interface PHShebangPreprocessor : NSObject @end diff --git a/Phoenix/PHShebangPreprocessor.m b/Phoenix/PHShebangPreprocessor.m index 84500334..e9d2fcd4 100644 --- a/Phoenix/PHShebangPreprocessor.m +++ b/Phoenix/PHShebangPreprocessor.m @@ -6,43 +6,48 @@ @implementation PHShebangPreprocessor -#pragma mark - Script Preprocessing +#pragma mark - Preprocessing -+ (nullable NSString *)preprocessScriptIfNeeded:(nonnull NSString *)script atPath:(NSString * __nonnull)path errorMessage:(NSString * _Nullable * _Nonnull)errorMessage { ++ (NSString *) process:(NSString *)script atPath:(NSString *)path error:(NSError **)error { NSScanner *scanner = [NSScanner scannerWithString:script]; - /* Return early if no shebang "#!" */ - if (![scanner scanString:@"#!" intoString:NULL]) { + // Shebang #! was not found + if (![scanner scanString:@"#!" intoString:nil]) { return script; } - NSString *preprocessCommand; - [scanner scanUpToCharactersFromSet:[NSCharacterSet newlineCharacterSet] intoString:&preprocessCommand]; - - NSPipe *stdoutPipe = [NSPipe pipe]; - NSPipe *stderrPipe = [NSPipe pipe]; - NSTask *task = [[NSTask alloc] init]; - [task setLaunchPath:@"/bin/sh"]; - [task setStandardOutput:stdoutPipe]; - [task setStandardError:stderrPipe]; + NSPipe *standardOutput = [NSPipe pipe]; + NSPipe *standardError = [NSPipe pipe]; + NSFileHandle *standardOutputFile = standardOutput.fileHandleForReading; + + NSString *command; + [scanner scanUpToCharactersFromSet:[NSCharacterSet newlineCharacterSet] intoString:&command]; - [task setArguments:@[@"-c", [NSString stringWithFormat:@"%@ %@", preprocessCommand, path]]]; + task.launchPath = @"/bin/bash"; + task.standardOutput = standardOutput; + task.standardError = standardError; + task.arguments = @[ @"-c", [NSString stringWithFormat:@"%@ %@", command, path] ]; - NSFileHandle *stdoutFile = [stdoutPipe fileHandleForReading]; - NSFileHandle *stderrFile = [stderrPipe fileHandleForReading]; [task launch]; - NSData *errorData = [stderrFile readDataToEndOfFile]; + NSData *errorData = [standardError.fileHandleForReading readDataToEndOfFile]; + + // Command resulted in error if (errorData.length > 0) { - *errorMessage = [[NSString alloc] initWithData:errorData encoding:NSUTF8StringEncoding]; + + NSString *description = [[NSString alloc] initWithData:errorData encoding:NSUTF8StringEncoding]; + + *error = [NSError errorWithDomain:PHShebangPreprocessorErrorDomain + code:PHShebangPreprocessorErrorCode + userInfo:@{ NSLocalizedDescriptionKey: description }]; } - /* Read past the shebang line to prevent syntax error */ - [stdoutFile readDataOfLength:2 + preprocessCommand.length]; + // Read past shebang #! + [standardOutputFile readDataOfLength:2 + command.length]; - return [[NSString alloc] initWithData:[stdoutFile readDataToEndOfFile] encoding:NSUTF8StringEncoding]; + return [[NSString alloc] initWithData:[standardOutputFile readDataToEndOfFile] encoding:NSUTF8StringEncoding]; } @end