diff --git a/ActionExecutor.h b/ActionExecutor.h index ed09355..c21d196 100644 --- a/ActionExecutor.h +++ b/ActionExecutor.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -28,17 +28,17 @@ #import #import "ActionProtocol.h" +#import "ExecutionOptions.h" @interface ActionExecutor : NSObject { } -+(void)executeActions:(NSArray *)actions - inMode:(unsigned)mode - waitingMilliseconds:(int)milliseconds; ++ (void)executeActions:(NSArray *)actions + withOptions:(struct ExecutionOptions)options; -+(NSDictionary *)shortcuts; ++ (NSDictionary *)shortcuts; -+(NSArray *)actionClasses; ++ (NSArray *)actionClasses; @end diff --git a/ActionExecutor.m b/ActionExecutor.m index 03c2dbc..9cbdba0 100644 --- a/ActionExecutor.m +++ b/ActionExecutor.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -28,34 +28,34 @@ #import "ActionExecutor.h" #include "ActionClassesMacro.h" +#include "ExecutionOptions.h" @implementation ActionExecutor -+(void)executeActions:(NSArray *)actions - inMode:(unsigned)mode - waitingMilliseconds:(int)milliseconds { ++ (void)executeActions:(NSArray *)actions + withOptions:(struct ExecutionOptions)options { NSDictionary *shortcuts = [self shortcuts]; struct timespec waitingtime; waitingtime.tv_sec = 0; - - if (milliseconds < 30) { - milliseconds = 30; + + if (options.waitTime < 100) { + options.waitTime = 100; } - if (milliseconds > 999) { - waitingtime.tv_sec = (int)floor(milliseconds / 1000); - waitingtime.tv_nsec = (milliseconds - waitingtime.tv_sec * 1000) * 1000000; + if (options.waitTime > 999) { + waitingtime.tv_sec = (int)floor(options.waitTime / 1000); + waitingtime.tv_nsec = (options.waitTime - waitingtime.tv_sec * 1000) * 1000000; } else { waitingtime.tv_sec = 0; - waitingtime.tv_nsec = milliseconds * 1000000; + waitingtime.tv_nsec = options.waitTime * 1000000; } - + NSUInteger i, count = [actions count]; for (i = 0; i < count; i++) { NSArray *action = [[actions objectAtIndex:i] componentsSeparatedByString:@":"]; - NSString *actionClass = [shortcuts objectForKey:[action objectAtIndex:0]]; + Class actionClass = [shortcuts objectForKey:[action objectAtIndex:0]]; if (nil == actionClass) { if ([[action objectAtIndex:0] isEqualToString:[actions objectAtIndex:i]]) { [NSException raise:@"InvalidCommandException" @@ -65,47 +65,55 @@ +(void)executeActions:(NSArray *)actions format:@"Unrecognized action shortcut “%@” in “%@”", [action objectAtIndex:0], [actions objectAtIndex:i]]; } } - - id actionClassInstance = [[NSClassFromString(actionClass) alloc] init]; - + + id actionClassInstance = [[actionClass alloc] init]; + if (![actionClassInstance conformsToProtocol:@protocol(ActionProtocol)]) { [NSException raise:@"InvalidCommandException" format:@"%@ does not conform to ActionProtocol", actionClass]; } - + + options.isFirstAction = i == 0; + options.isLastAction = i == count - 1; + if ([action count] > 1) { - [actionClassInstance performActionWithData:[[action subarrayWithRange:NSMakeRange(1, [action count] - 1)] componentsJoinedByString:@":"] inMode:mode]; + [actionClassInstance performActionWithData:[[action subarrayWithRange:NSMakeRange(1, [action count] - 1)] componentsJoinedByString:@":"] + withOptions:options]; } else { - [actionClassInstance performActionWithData:@"" inMode:mode]; + [actionClassInstance performActionWithData:@"" + withOptions:options]; } - + [actionClassInstance release]; - nanosleep(&waitingtime, NULL); + if (!options.isLastAction) { + nanosleep(&waitingtime, NULL); + } } } -+(NSArray *)actionClasses { ++ (NSArray *)actionClasses { NSArray *actionClasses = [NSArray arrayWithObjects:ACTION_CLASSES]; return actionClasses; } -+(NSDictionary *)shortcuts { - ++ (NSDictionary *)shortcuts { + NSArray *actionClasses = [[self class] actionClasses]; NSMutableDictionary *shortcuts = [NSMutableDictionary dictionaryWithCapacity:[actionClasses count]]; - NSUInteger i, ii; - + NSUInteger i, ii; + for (i = 0, ii = [actionClasses count]; i < ii; i++) { NSString *classname = [actionClasses objectAtIndex:i]; - NSString *shortcut = [NSClassFromString(classname) commandShortcut]; + Class actionClass = NSClassFromString(classname); + NSString *shortcut = [actionClass commandShortcut]; if (nil != [shortcuts objectForKey:shortcut]) { [NSException raise:@"ShortcutConflictException" format:@"Shortcut “%@” is used by more than one action class", shortcut]; - } - [shortcuts setObject:classname forKey:shortcut]; + } + [shortcuts setObject:actionClass forKey:shortcut]; } - + return [[shortcuts retain] autorelease]; } diff --git a/Actions/ActionProtocol.h b/Actions/ActionProtocol.h index eec3348..2c861e6 100644 --- a/Actions/ActionProtocol.h +++ b/Actions/ActionProtocol.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -27,35 +27,38 @@ */ #import +#import "ExecutionOptions.h" @protocol ActionProtocol /** - Returns the command's shortcut. - - The command shortcut is the string which has to be used as command-line argument (typically followed by “:” plus some arguments) to invoke the command. - @note The command shortcut is unique for each command. - @return Command shortcut + * Returns the command's shortcut + * + * The command shortcut is the string which has to be used as command-line argument (typically followed by “:” plus some arguments) to invoke the command. + * + * @note The command shortcut has to be unique for each command. + * @return Command shortcut */ -+(NSString *)commandShortcut; ++ (NSString *)commandShortcut; /** - Returns the command description - - The command description is to be included in the help output and is formatted (i.e.: indented). It should include a description as well as at least one usage example. - @return Command shortcut + * Returns the command description + * + * The command description is to be included in the help output and is formatted (i.e.: indented). It should include a description as well as at least one usage example. + * + * @return Command description */ -+(NSString *)commandDescription; ++ (NSString *)commandDescription; /** - Performs the action - - Depending on the `mode` argument, this can be the action, printing a description of the action to STDOUT or both. - - @param data Part of the argument remaining after stripping the leading command identifier - @param mode One of: MODE_VERBOSE, MODE_TEST, MODE_REGULAR + * Performs the action + * + * Depending on the `mode` argument, this can be the action, printing a description of the action to STDOUT or both. + * + * @param data Part of the argument remaining after stripping the leading command identifier + * @param options */ --(void)performActionWithData:(NSString *)data - inMode:(unsigned)mode; +- (void)performActionWithData:(NSString *)data + withOptions:(struct ExecutionOptions)options; @end diff --git a/Actions/ClickAction.h b/Actions/ClickAction.h index 49bec79..0681499 100644 --- a/Actions/ClickAction.h +++ b/Actions/ClickAction.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -34,12 +34,12 @@ } -+(NSString *)commandShortcut; ++ (NSString *)commandShortcut; -+(NSString *)commandDescription; ++ (NSString *)commandDescription; --(NSString *)actionDescriptionString:(NSString *)locationDescription; +- (NSString *)actionDescriptionString:(NSString *)locationDescription; --(void)performActionAtPoint:(CGPoint) p; +- (void)performActionAtPoint:(CGPoint) p; @end diff --git a/Actions/ClickAction.m b/Actions/ClickAction.m index 580d0a2..457f473 100644 --- a/Actions/ClickAction.m +++ b/Actions/ClickAction.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -27,16 +27,17 @@ */ #import "ClickAction.h" +#include @implementation ClickAction #pragma mark - ActionProtocol -+(NSString *)commandShortcut { ++ (NSString *)commandShortcut { return @"c"; } -+(NSString *)commandDescription { ++ (NSString *)commandDescription { return @" c:x,y Will CLICK at the point with the given coordinates.\n" " Example: “c:12,34” will click at the point with x coordinate\n" " 12 and y coordinate 34. Instead of x and y values, you may\n" @@ -46,16 +47,18 @@ +(NSString *)commandDescription { #pragma mark - MouseBaseAction --(NSString *)actionDescriptionString:(NSString *)locationDescription { +- (NSString *)actionDescriptionString:(NSString *)locationDescription { return [NSString stringWithFormat:@"Click at %@", locationDescription]; } --(void)performActionAtPoint:(CGPoint) p { +- (void)performActionAtPoint:(CGPoint) p { // Left button down CGEventRef leftDown = CGEventCreateMouseEvent(NULL, kCGEventLeftMouseDown, CGPointMake(p.x, p.y), kCGMouseButtonLeft); CGEventPost(kCGHIDEventTap, leftDown); CFRelease(leftDown); + usleep(15000); // Improve reliability + // Left button up CGEventRef leftUp = CGEventCreateMouseEvent(NULL, kCGEventLeftMouseUp, CGPointMake(p.x, p.y), kCGMouseButtonLeft); CGEventPost(kCGHIDEventTap, leftUp); diff --git a/Actions/ColorPickerAction.h b/Actions/ColorPickerAction.h index 5e4691c..1eb2156 100644 --- a/Actions/ColorPickerAction.h +++ b/Actions/ColorPickerAction.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -33,11 +33,11 @@ } -+(NSString *)commandShortcut; ++ (NSString *)commandShortcut; -+(NSString *)commandDescription; ++ (NSString *)commandDescription; --(void)performActionWithData:(NSString *)data - inMode:(unsigned)mode; +- (void)performActionWithData:(NSString *)data + withOptions:(struct ExecutionOptions)options; @end diff --git a/Actions/ColorPickerAction.m b/Actions/ColorPickerAction.m index 989a37a..af048c8 100644 --- a/Actions/ColorPickerAction.m +++ b/Actions/ColorPickerAction.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -33,19 +33,19 @@ @implementation ColorPickerAction #pragma mark - ActionProtocol -+(NSString *)commandShortcut { ++ (NSString *)commandShortcut { return @"cp"; } -+(NSString *)commandDescription { ++ (NSString *)commandDescription { return @" cp:str Will PRINT THE COLOR value at the given screen location.\n" " The color value is printed as three decimal 8-bit values,\n" " representing, in order, red, green, and blue.\n" " Example: “cp:123,456” might print “127 63 0”"; } --(void)performActionWithData:(NSString *)data - inMode:(unsigned)mode { +- (void)performActionWithData:(NSString *)data + withOptions:(struct ExecutionOptions)options { NSString *shortcut = [[self class] commandShortcut]; @@ -71,11 +71,11 @@ -(void)performActionWithData:(NSString *)data } } - if (MODE_TEST == mode) { + if (MODE_TEST == options.mode) { if ([data isEqualToString:@"."]) { - printf("Print color at current mouse position\n"); + [options.verbosityOutputHandler write:@"Print color at current mouse position"]; } else { - printf("Print color at location %i,%i\n", [[coords objectAtIndex:0] intValue], [[coords objectAtIndex:1] intValue]); + [options.verbosityOutputHandler write:[NSString stringWithFormat:@"Print color at location %i,%i\n", [[coords objectAtIndex:0] intValue], [[coords objectAtIndex:1] intValue]]]; } } else { CGPoint p; @@ -89,10 +89,9 @@ -(void)performActionWithData:(NSString *)data NSColor *color = [bitmap colorAtX:0 y:0]; [bitmap release]; - printf("%d %d %d\n", (int)(color.redComponent*255), (int)(color.greenComponent*255), (int)(color.blueComponent*255)); + [options.commandOutputHandler write:[NSString stringWithFormat:@"%d %d %d\n", (int)(color.redComponent*255), (int)(color.greenComponent*255), (int)(color.blueComponent*255)]]; } } - } @end diff --git a/Actions/DoubleclickAction.h b/Actions/DoubleclickAction.h index 7e5af38..96df070 100644 --- a/Actions/DoubleclickAction.h +++ b/Actions/DoubleclickAction.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -34,10 +34,10 @@ } -+(NSString *)commandShortcut; ++ (NSString *)commandShortcut; --(NSString *)actionDescriptionString:(NSString *)locationDescription; +- (NSString *)actionDescriptionString:(NSString *)locationDescription; --(void)performActionAtPoint:(CGPoint) p; +- (void)performActionAtPoint:(CGPoint) p; @end diff --git a/Actions/DoubleclickAction.m b/Actions/DoubleclickAction.m index 48d4ae0..9c566de 100644 --- a/Actions/DoubleclickAction.m +++ b/Actions/DoubleclickAction.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -33,30 +33,29 @@ @implementation DoubleclickAction #pragma mark - ActionProtocol -+(NSString *)commandShortcut { ++ (NSString *)commandShortcut { return @"dc"; } -+(NSString *)commandDescription { ++ (NSString *)commandDescription { return @" dc:x,y Will DOUBLE-CLICK at the point with the given coordinates.\n" " Example: “dc:12,34” will double-click at the point with x\n" " coordinate 12 and y coordinate 34. Instead of x and y values,\n" " you may also use “.”, which means: the current position."; } - #pragma mark - MouseBaseAction --(NSString *)actionDescriptionString:(NSString *)locationDescription { +- (NSString *)actionDescriptionString:(NSString *)locationDescription { return [NSString stringWithFormat:@"Double-click at %@", locationDescription]; } --(void)performActionAtPoint:(CGPoint) p { - +- (void)performActionAtPoint:(CGPoint) p { + // Left button down CGEventRef mouseEvent = CGEventCreateMouseEvent(NULL, kCGEventLeftMouseDown, CGPointMake(p.x, p.y), kCGMouseButtonLeft); CGEventPost(kCGHIDEventTap, mouseEvent); - + // Left button up CGEventSetType(mouseEvent, kCGEventLeftMouseUp); CGEventPost(kCGHIDEventTap, mouseEvent); @@ -65,14 +64,14 @@ -(void)performActionAtPoint:(CGPoint) p { // 2nd click CGEventSetIntegerValueField(mouseEvent, kCGMouseEventClickState, 2); - - CGEventSetType(mouseEvent, kCGEventLeftMouseDown); - CGEventPost(kCGHIDEventTap, mouseEvent); - - CGEventSetType(mouseEvent, kCGEventLeftMouseUp); - CGEventPost(kCGHIDEventTap, mouseEvent); - - CFRelease(mouseEvent); + + CGEventSetType(mouseEvent, kCGEventLeftMouseDown); + CGEventPost(kCGHIDEventTap, mouseEvent); + + CGEventSetType(mouseEvent, kCGEventLeftMouseUp); + CGEventPost(kCGHIDEventTap, mouseEvent); + + CFRelease(mouseEvent); } @end diff --git a/Actions/DragDownAction.h b/Actions/DragDownAction.h index deec852..c62b4d2 100644 --- a/Actions/DragDownAction.h +++ b/Actions/DragDownAction.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -31,15 +31,15 @@ #import "MouseBaseAction.h" @interface DragDownAction : MouseBaseAction { - + } -+(NSString *)commandShortcut; ++ (NSString *)commandShortcut; -+(NSString *)commandDescription; ++ (NSString *)commandDescription; --(NSString *)actionDescriptionString:(NSString *)locationDescription; +- (NSString *)actionDescriptionString:(NSString *)locationDescription; --(void)performActionAtPoint:(CGPoint) p; +- (void)performActionAtPoint:(CGPoint) p; @end diff --git a/Actions/DragDownAction.m b/Actions/DragDownAction.m index e23e564..041613f 100644 --- a/Actions/DragDownAction.m +++ b/Actions/DragDownAction.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -32,26 +32,25 @@ @implementation DragDownAction #pragma mark - ActionProtocol -+(NSString *)commandShortcut { ++ (NSString *)commandShortcut { return @"dd"; } -+(NSString *)commandDescription { ++ (NSString *)commandDescription { return @" dd:x,y Will press down to START A DRAG at the given coordinates.\n" " Example: “dd:12,34” will press down at the point with x\n" " coordinate 12 and y coordinate 34. Instead of x and y values,\n" " you may also use “.”, which means: the current position."; } - #pragma mark - MouseBaseAction --(NSString *)actionDescriptionString:(NSString *)locationDescription { +- (NSString *)actionDescriptionString:(NSString *)locationDescription { return [NSString stringWithFormat:@"Drag press down at %@", locationDescription]; } --(void)performActionAtPoint:(CGPoint) p { - // Left button down +- (void)performActionAtPoint:(CGPoint) p { + // Left button down, but don't release CGEventRef leftDown = CGEventCreateMouseEvent(NULL, kCGEventLeftMouseDown, CGPointMake(p.x, p.y), kCGMouseButtonLeft); CGEventPost(kCGHIDEventTap, leftDown); CFRelease(leftDown); diff --git a/Actions/DragUpAction.h b/Actions/DragUpAction.h index 2727e96..b329bf3 100644 --- a/Actions/DragUpAction.h +++ b/Actions/DragUpAction.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -31,15 +31,15 @@ #import "MouseBaseAction.h" @interface DragUpAction : MouseBaseAction { - + } -+(NSString *)commandShortcut; ++ (NSString *)commandShortcut; -+(NSString *)commandDescription; ++ (NSString *)commandDescription; --(NSString *)actionDescriptionString:(NSString *)locationDescription; +- (NSString *)actionDescriptionString:(NSString *)locationDescription; --(void)performActionAtPoint:(CGPoint) p; +- (void)performActionAtPoint:(CGPoint) p; @end diff --git a/Actions/DragUpAction.m b/Actions/DragUpAction.m index 0aeb5c0..ce51e0d 100644 --- a/Actions/DragUpAction.m +++ b/Actions/DragUpAction.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -33,11 +33,11 @@ @implementation DragUpAction #pragma mark - ActionProtocol -+(NSString *)commandShortcut { ++ (NSString *)commandShortcut { return @"du"; } -+(NSString *)commandDescription { ++ (NSString *)commandDescription { return @" du:x,y Will release to END A DRAG at the given coordinates.\n" " Example: “du:112,134” will release at the point with x\n" " coordinate 112 and y coordinate 134."; @@ -45,24 +45,19 @@ +(NSString *)commandDescription { #pragma mark - MouseBaseAction --(NSString *)actionDescriptionString:(NSString *)locationDescription { +- (NSString *)actionDescriptionString:(NSString *)locationDescription { return [NSString stringWithFormat:@"Drag release at %@", locationDescription]; } --(void)performActionAtPoint:(CGPoint) p { - // Left button dragged - CGEventRef leftDragged = CGEventCreateMouseEvent(NULL, kCGEventLeftMouseDragged, CGPointMake(p.x, p.y), kCGMouseButtonLeft); - CGEventPost(kCGHIDEventTap, leftDragged); - CFRelease(leftDragged); - - usleep(200000); // Some target applications require this to recognize the drop - +- (void)performActionAtPoint:(CGPoint) p { // Left button up CGEventRef leftUp = CGEventCreateMouseEvent(NULL, kCGEventLeftMouseUp, CGPointMake(p.x, p.y), kCGMouseButtonLeft); CGEventPost(kCGHIDEventTap, leftUp); CFRelease(leftUp); +} - usleep(800000); // This delay should usually prevent that a subsequent "wait" command is necessary +- (uint32_t)getMoveEventConstant { + return kCGEventLeftMouseDragged; } @end diff --git a/Actions/KeyBaseAction.h b/Actions/KeyBaseAction.h index ad8465e..501d856 100644 --- a/Actions/KeyBaseAction.h +++ b/Actions/KeyBaseAction.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -28,62 +28,57 @@ #import #import "ActionProtocol.h" +#import "ExecutionOptions.h" @interface KeyBaseAction : NSObject { } /** - Returns the keys that are supported by the command. - - @return An NSDictionary which has keyboard key name as dictionary keys and keyboard key codes (strings) as dictionary values. + * Returns the keys that are supported by the command. + * + * @return An NSDictionary which has keyboard key name as dictionary keys and keyboard key codes (strings) as dictionary values. */ -+(NSDictionary *)getSupportedKeycodes; - ++ (NSDictionary *)getSupportedKeycodes; /** - Returns the list of keys supported by the command - - @param indent String to use as indentation string at the beginning of each line - - @return Newline-separated string + * Returns the list of keys supported by the command + * + * @param indent String to use as indentation string at the beginning of each line + * + * @return Newline-separated string */ -+(NSString *)getSupportedKeysIndentedWith:(NSString *)indent; - ++ (NSString *)getSupportedKeysIndentedWith:(NSString *)indent; /** - Returns a string describing the action performed be the command - - @param keyName Name of the key - - @return Human-readable phrase such as @@"Press blahblah key" - - @note This method must be overwritten by subclasses + * Returns a string describing the action performed be the command + * + * @param keyName Name of the key + * @return Human-readable phrase such as @@"Press blahblah key" + * @note This method must be overwritten by subclasses */ --(NSString *)actionDescriptionString:(NSString *)keyName; - +- (NSString *)actionDescriptionString:(NSString *)keyName; /** - Performs the command's action - - @param code The key code - - @note This method must be overwritten by subclasses + * Performs the command's action + * + * @param code The key code + * + * @note This method must be overwritten by subclasses */ --(void)performActionWithKeycode:(CGKeyCode)code; - +- (void)performActionWithKeycode:(CGKeyCode)code; #pragma mark - ActionProtocol /** - Performs the action - - Depending on the `mode` argument, this can be the action, printing a description of the action to STDOUT or both. - - @param data Part of the argument remaining after stripping the leading command identifier - @param mode One of: MODE_VERBOSE, MODE_TEST, MODE_REGULAR + * Performs the action + * + * Depending on the `mode` argument, this can be the action, printing a description of the action to STDOUT or both. + * + * @param data Part of the argument remaining after stripping the leading command identifier + * @param options */ --(void)performActionWithData:(NSString *)data - inMode:(unsigned)mode; +- (void)performActionWithData:(NSString *)data + withOptions:(struct ExecutionOptions)options; @end diff --git a/Actions/KeyBaseAction.m b/Actions/KeyBaseAction.m index 3d10923..9288f59 100644 --- a/Actions/KeyBaseAction.m +++ b/Actions/KeyBaseAction.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -27,16 +27,17 @@ */ #import "KeyBaseAction.h" +#import "ExecutionOptions.h" @implementation KeyBaseAction -+(NSDictionary *)getSupportedKeycodes { ++ (NSDictionary *)getSupportedKeycodes { [NSException raise:@"InvalidCommandException" format:@"To be implemented by subclasses"]; return [NSDictionary dictionaryWithObject:@"Will never be reached, but makes Xcode happy" forKey:@"Foo"]; } -+(NSString *)getSupportedKeysIndentedWith:(NSString *)indent { ++ (NSString *)getSupportedKeysIndentedWith:(NSString *)indent { NSArray *sortedkeyNames = [[[[self class] getSupportedKeycodes] allKeys] sortedArrayUsingComparator:^(id obj1, id obj2) { return [obj1 compare:obj2 options:NSNumericSearch]; @@ -45,27 +46,31 @@ +(NSString *)getSupportedKeysIndentedWith:(NSString *)indent { return [NSString stringWithFormat:@"%@%@", indent, [sortedkeyNames componentsJoinedByString:[@"\n" stringByAppendingString:indent]]]; } --(NSString *)actionDescriptionString:(NSString *)keyName { +- (NSString *)actionDescriptionString:(NSString *)keyName { [NSException raise:@"InvalidCommandException" format:@"To be implemented by subclasses"]; return @"Will never be reached, but makes Xcode happy"; } --(void)performActionWithKeycode:(CGKeyCode)code { +- (void)performActionWithKeycode:(CGKeyCode)code { [NSException raise:@"InvalidCommandException" format:@"To be implemented by subclasses"]; } #pragma mark - ActionProtocol --(void)performActionWithData:(NSString *)data - inMode:(unsigned)mode { +- (void)performActionWithData:(NSString *)data + withOptions:(struct ExecutionOptions)options { NSString *shortcut = [[self class] commandShortcut]; + // Wait before executing the key event(s). If this is the very first action, use a longer + // delay (as it could be observed that an initial keyboard was swallowed, cf. issue #39), + // otherwise only a short delay. struct timespec waitingtime; waitingtime.tv_sec = 0; - waitingtime.tv_nsec = 5 * 1000000; // Milliseconds + waitingtime.tv_nsec = (options.isFirstAction ? 65 : 20) * 1000000; // Milliseconds + nanosleep(&waitingtime, NULL); if ([data isEqualToString:@""]) { [NSException raise:@"InvalidCommandException" @@ -74,8 +79,8 @@ -(void)performActionWithData:(NSString *)data } NSDictionary *keycodes = [[self class] getSupportedKeycodes]; - NSArray *keys = [data componentsSeparatedByString:@","]; - NSUInteger i, count = [keys count]; + NSArray *keys = [data componentsSeparatedByString:@","]; + NSUInteger i, count = [keys count]; // First, validate the key names for (i = 0; i < count; i++) { @@ -86,18 +91,16 @@ -(void)performActionWithData:(NSString *)data keyname, shortcut, [[self class] getSupportedKeysIndentedWith:@" - "]]; } } - + // Then, perform whatever action is requested for (i = 0; i < count; i++) { unsigned code = [[keycodes objectForKey:[keys objectAtIndex:i]] intValue]; - if (MODE_REGULAR != mode) { - NSString *description = [self actionDescriptionString:[keys objectAtIndex:i]]; - printf("%s\n", [description UTF8String]); + if (MODE_REGULAR != options.mode) { + [options.verbosityOutputHandler write:[self actionDescriptionString:[keys objectAtIndex:i]]]; } - if (MODE_TEST != mode) { - nanosleep(&waitingtime, NULL); + if (MODE_TEST != options.mode) { [self performActionWithKeycode:(CGKeyCode)code]; } } diff --git a/Actions/KeyDownAction.h b/Actions/KeyDownAction.h index a70aac4..055c85c 100644 --- a/Actions/KeyDownAction.h +++ b/Actions/KeyDownAction.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/Actions/KeyDownAction.m b/Actions/KeyDownAction.m index bd48e15..406f87b 100644 --- a/Actions/KeyDownAction.m +++ b/Actions/KeyDownAction.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -32,11 +32,11 @@ @implementation KeyDownAction #pragma mark - ActionProtocol -+(NSString *)commandShortcut { ++ (NSString *)commandShortcut { return @"kd"; } -+(NSString *)commandDescription { ++ (NSString *)commandDescription { NSString *keyList = [[self class] getSupportedKeysIndentedWith:@" - "]; NSString *format = @" kd:keys Will trigger a KEY DOWN event for a comma-separated list of\n" " modifier keys. Possible keys are:\n%@\n" @@ -48,13 +48,13 @@ +(NSString *)commandDescription { #pragma mark - KeyBaseAction --(void)performActionWithKeycode:(CGKeyCode)code { +- (void)performActionWithKeycode:(CGKeyCode)code { CGEventRef e = CGEventCreateKeyboardEvent(NULL, code, true); CGEventPost(kCGSessionEventTap, e); CFRelease(e); } --(NSString *)actionDescriptionString:(NSString *)keyName { +- (NSString *)actionDescriptionString:(NSString *)keyName { return [NSString stringWithFormat:@"Hold %@ key down", keyName]; } diff --git a/Actions/KeyDownUpBaseAction.h b/Actions/KeyDownUpBaseAction.h index 5879bc5..b56d7ee 100644 --- a/Actions/KeyDownUpBaseAction.h +++ b/Actions/KeyDownUpBaseAction.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/Actions/KeyDownUpBaseAction.m b/Actions/KeyDownUpBaseAction.m index a93fe0f..7a5239f 100644 --- a/Actions/KeyDownUpBaseAction.m +++ b/Actions/KeyDownUpBaseAction.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -32,7 +32,7 @@ @implementation KeyDownUpBaseAction #pragma mark - KeyBaseAction -+(NSDictionary *)getSupportedKeycodes { ++ (NSDictionary *)getSupportedKeycodes { return [NSDictionary dictionaryWithObjectsAndKeys: @"59", @"ctrl", @"55", @"cmd", diff --git a/Actions/KeyPressAction.h b/Actions/KeyPressAction.h index cd3758c..b2e155a 100644 --- a/Actions/KeyPressAction.h +++ b/Actions/KeyPressAction.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/Actions/KeyPressAction.m b/Actions/KeyPressAction.m index 7dfd70c..f068754 100644 --- a/Actions/KeyPressAction.m +++ b/Actions/KeyPressAction.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -32,11 +32,11 @@ @implementation KeyPressAction #pragma mark - ActionProtocol -+(NSString *)commandShortcut { ++ (NSString *)commandShortcut { return @"kp"; } -+(NSString *)commandDescription { ++ (NSString *)commandDescription { NSString *keyList = [[self class] getSupportedKeysIndentedWith:@" - "]; NSString *format = @" kp:key Will emulate PRESSING A KEY (key down + key up). Possible keys are:\n%@\n" " Example: “kp:return” will hit the return key."; @@ -45,7 +45,7 @@ +(NSString *)commandDescription { #pragma mark - KeyBaseAction -+(NSDictionary *)getSupportedKeycodes { ++ (NSDictionary *)getSupportedKeycodes { return [NSDictionary dictionaryWithObjectsAndKeys: // See /System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h @"36", @"return", @@ -100,21 +100,21 @@ +(NSDictionary *)getSupportedKeycodes { @"76", @"num-enter", // "NSSystemDefined" events, see list in IOKit/hidsystem/ev_keymap.h - [NSString stringWithFormat:@"%i", NX_KEYTYPE_MUTE], @"mute", - [NSString stringWithFormat:@"%i", NX_KEYTYPE_SOUND_UP], @"volume-up", - [NSString stringWithFormat:@"%i", NX_KEYTYPE_SOUND_DOWN], @"volume-down", - [NSString stringWithFormat:@"%i", NX_KEYTYPE_BRIGHTNESS_UP], @"brightness-up", - [NSString stringWithFormat:@"%i", NX_KEYTYPE_BRIGHTNESS_DOWN], @"brightness-down", - [NSString stringWithFormat:@"%i", NX_KEYTYPE_PLAY], @"play-pause", - [NSString stringWithFormat:@"%i", NX_KEYTYPE_PREVIOUS], @"play-previous", - [NSString stringWithFormat:@"%i", NX_KEYTYPE_NEXT], @"play-next", - [NSString stringWithFormat:@"%i", NX_KEYTYPE_ILLUMINATION_TOGGLE], @"keys-light-toggle", - [NSString stringWithFormat:@"%i", NX_KEYTYPE_ILLUMINATION_UP], @"keys-light-up", - [NSString stringWithFormat:@"%i", NX_KEYTYPE_ILLUMINATION_DOWN], @"keys-light-down", + [NSString stringWithFormat:@"%i", NX_KEYTYPE_MUTE], @"mute", + [NSString stringWithFormat:@"%i", NX_KEYTYPE_SOUND_UP], @"volume-up", + [NSString stringWithFormat:@"%i", NX_KEYTYPE_SOUND_DOWN], @"volume-down", + [NSString stringWithFormat:@"%i", NX_KEYTYPE_BRIGHTNESS_UP], @"brightness-up", + [NSString stringWithFormat:@"%i", NX_KEYTYPE_BRIGHTNESS_DOWN], @"brightness-down", + [NSString stringWithFormat:@"%i", NX_KEYTYPE_PLAY], @"play-pause", + [NSString stringWithFormat:@"%i", NX_KEYTYPE_PREVIOUS], @"play-previous", + [NSString stringWithFormat:@"%i", NX_KEYTYPE_NEXT], @"play-next", + [NSString stringWithFormat:@"%i", NX_KEYTYPE_ILLUMINATION_TOGGLE], @"keys-light-toggle", + [NSString stringWithFormat:@"%i", NX_KEYTYPE_ILLUMINATION_UP], @"keys-light-up", + [NSString stringWithFormat:@"%i", NX_KEYTYPE_ILLUMINATION_DOWN], @"keys-light-down", nil]; } --(BOOL)keyCodeRequiresSystemDefinedEvent:(CGKeyCode)code { +- (BOOL)keyCodeRequiresSystemDefinedEvent:(CGKeyCode)code { return code == NX_KEYTYPE_SOUND_UP || code == NX_KEYTYPE_SOUND_DOWN || code == NX_KEYTYPE_MUTE || @@ -130,7 +130,7 @@ -(BOOL)keyCodeRequiresSystemDefinedEvent:(CGKeyCode)code { ; } --(void)performActionWithKeycode:(CGKeyCode)code { +- (void)performActionWithKeycode:(CGKeyCode)code { if ([self keyCodeRequiresSystemDefinedEvent:code]) { NSEvent *e1 = [NSEvent otherEventWithType:NSSystemDefined @@ -165,7 +165,7 @@ -(void)performActionWithKeycode:(CGKeyCode)code { } } --(NSString *)actionDescriptionString:(NSString *)keyName { +- (NSString *)actionDescriptionString:(NSString *)keyName { return [NSString stringWithFormat:@"Press + release %@ key", keyName]; } diff --git a/Actions/KeyUpAction.h b/Actions/KeyUpAction.h index f21ccbd..2762147 100644 --- a/Actions/KeyUpAction.h +++ b/Actions/KeyUpAction.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -31,7 +31,7 @@ #import "KeyDownUpBaseAction.h" @interface KeyUpAction : KeyDownUpBaseAction { - + } @end diff --git a/Actions/KeyUpAction.m b/Actions/KeyUpAction.m index 8aeb4db..6d0a1d1 100644 --- a/Actions/KeyUpAction.m +++ b/Actions/KeyUpAction.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -32,11 +32,11 @@ @implementation KeyUpAction #pragma mark - ActionProtocol -+(NSString *)commandShortcut { ++ (NSString *)commandShortcut { return @"ku"; } -+(NSString *)commandDescription { ++ (NSString *)commandDescription { NSString *keyList = [[self class] getSupportedKeysIndentedWith:@" - "]; NSString *format = @" ku:keys Will trigger a KEY UP event for a comma-separated list of\n" " modifier keys. Possible keys are:\n%@\n" @@ -48,13 +48,13 @@ +(NSString *)commandDescription { #pragma mark - KeyBaseAction --(void)performActionWithKeycode:(CGKeyCode)code { +- (void)performActionWithKeycode:(CGKeyCode)code { CGEventRef e = CGEventCreateKeyboardEvent(NULL, code, false); CGEventPost(kCGSessionEventTap, e); CFRelease(e); } --(NSString *)actionDescriptionString:(NSString *)keyName { +- (NSString *)actionDescriptionString:(NSString *)keyName { return [NSString stringWithFormat:@"Release %@ key", keyName]; } diff --git a/Actions/MouseBaseAction.h b/Actions/MouseBaseAction.h index c70536a..f32df47 100644 --- a/Actions/MouseBaseAction.h +++ b/Actions/MouseBaseAction.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -39,54 +39,56 @@ typedef enum { } /** - Takes an unparsed position string for a single axis and returns the corresponding position - - @param unparsedValue String in one of the supported formats, such as @@"934", @@"+17" or @@"=218" - @param axis The axis + * Takes an unparsed position string for a single axis and returns the corresponding position + * + * @param unparsedValue String in one of the supported formats, such as @@"934", @@"+17" or @@"=218" + * @param axis The axis */ -+(int)getCoordinate:(NSString *)unparsedValue - forAxis:(CLICLICKAXIS)axis; ++ (int)getCoordinate:(NSString *)unparsedValue + forAxis:(CLICLICKAXIS)axis; /** - Checks if the given string is an acceptable X or Y coordinate value and throws an exception if not - - @param string String to test. See +getCoordinate:forAxis: for supported syntaxes - @param axis The axis -*/ -+(void)validateAxisValue:(NSString *)string - forAxis:(CLICLICKAXIS)axis; + * Checks if the given string is an acceptable X or Y coordinate value and throws an exception if not + * + * @param string String to test. See +getCoordinate:forAxis: for supported syntaxes + * @param axis The axis + */ ++ (void)validateAxisValue:(NSString *)string + forAxis:(CLICLICKAXIS)axis; /** - Returns a human-readable description of the action - - This should be a one-line string which will be used in “verbose” and in “test” mode. - - @param locationDescription A textual representation of the coordinates at which the action is performed. + * Returns a human-readable description of the action + * + * This should be a one-line string which will be used in “verbose” and in “test” mode. + * + * @param locationDescription A textual representation of the coordinates at which the action is performed. */ --(NSString *)actionDescriptionString:(NSString *)locationDescription; +- (NSString *)actionDescriptionString:(NSString *)locationDescription; /** - Performs the mouse-related action an inheriting command provides - - This method is called as last step of method performActionWithData:inMode: It should only perform the action, not print a description when in MODE_VERBOSE mode, as this is done by performActionWithData:inMode: - - @note This method will only be invoked when in MODE_REGULAR or MODE_VERBOSE mode. + * Performs the mouse-related action an inheriting command provides + * + * This method is called as last step of method performActionWithData:withOptions: It should only perform the action, not print a description when in MODE_VERBOSE mode, as this is done by performActionWithData:withOptions: + * + * @note This method will only be invoked when in MODE_REGULAR or MODE_VERBOSE mode. */ --(void)performActionAtPoint:(CGPoint)p; +- (void)performActionAtPoint:(CGPoint)p; /** - Performs the action - - Depending on the mode argument, this can be the action, printing a description of the action to STDOUT or both. This implementation performs the preparatory steps such as validating arguments, calculating the mouse position etc., but leaves performing the action to subclasses, whose performActionAtPoint: method it eventually invokes. - - @param data Part of the argument remaining after stripping the leading command identifier - @param mode One of: MODE_VERBOSE, MODE_TEST, MODE_REGULAR + * Performs the action + * + * Depending on the mode argument, this can be the action, printing a description of the action to STDOUT or both. This implementation performs the preparatory steps such as validating arguments, calculating the mouse position etc., but leaves performing the action to subclasses, whose performActionAtPoint: method it eventually invokes. + * + * @param data Part of the argument remaining after stripping the leading command identifier + * @param options */ --(void)performActionWithData:(NSString *)data - inMode:(unsigned)mode; +- (void)performActionWithData:(NSString *)data + withOptions:(struct ExecutionOptions)options; + +- (void)postHumanizedMouseEventsWithEasingFactor:(unsigned)easing + toX:(float)endX + toY:(float)endY; --(void)postHumanizedMouseEventsOfType:(CGEventType)eventType - toX:(float)endX - toY:(float)endY; +- (uint32_t)getMoveEventConstant; @end diff --git a/Actions/MouseBaseAction.m b/Actions/MouseBaseAction.m index 3233a85..12bee99 100644 --- a/Actions/MouseBaseAction.m +++ b/Actions/MouseBaseAction.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -33,8 +33,8 @@ @implementation MouseBaseAction -+(int)getCoordinate:(NSString *)unparsedValue - forAxis:(CLICLICKAXIS)axis { ++ (int)getCoordinate:(NSString *)unparsedValue + forAxis:(CLICLICKAXIS)axis { [[self class] validateAxisValue:unparsedValue forAxis:axis]; @@ -42,9 +42,9 @@ +(int)getCoordinate:(NSString *)unparsedValue [[unparsedValue substringToIndex:1] isEqualToString:@"-"]) { // Relative value CGEventRef dummyEvent = CGEventCreate(NULL); - CGPoint ourLoc = CGEventGetLocation(dummyEvent); - int positionDiff = [unparsedValue intValue]; - int currentPosition = axis == XAXIS ? (int)ourLoc.x : (int)ourLoc.y; + CGPoint ourLoc = CGEventGetLocation(dummyEvent); + int positionDiff = [unparsedValue intValue]; + int currentPosition = axis == XAXIS ? (int)ourLoc.x : (int)ourLoc.y; CFRelease(dummyEvent); return (int) currentPosition + positionDiff; @@ -59,8 +59,8 @@ +(int)getCoordinate:(NSString *)unparsedValue return [unparsedValue intValue]; } -+(void)validateAxisValue:(NSString *)string - forAxis:(CLICLICKAXIS)axis { ++ (void)validateAxisValue:(NSString *)string + forAxis:(CLICLICKAXIS)axis { NSString *regex = @"^=?[+-]?\\d+$"; NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", regex]; if ([predicate evaluateWithObject:string] != YES) { @@ -69,28 +69,28 @@ +(void)validateAxisValue:(NSString *)string } } --(NSString *)actionDescriptionString:(NSString *)locationDescription { +- (NSString *)actionDescriptionString:(NSString *)locationDescription { [NSException raise:@"InvalidCommandException" format:@"To be implemented by subclasses"]; return @"Will never be reached, but makes Xcode happy ;-)"; } --(void)performActionAtPoint:(CGPoint)p { +- (void)performActionAtPoint:(CGPoint)p { [NSException raise:@"InvalidCommandException" format:@"To be implemented by subclasses"]; } #pragma mark - ActionProtocol --(void)performActionWithData:(NSString *)data - inMode:(unsigned)mode { - +- (void)performActionWithData:(NSString *)data + withOptions:(struct ExecutionOptions)options { + CGPoint p; NSString *shortcut = [[self class] commandShortcut]; NSString *verboseLoc; - CGEventRef ourEvent = CGEventCreate(NULL); - CGPoint currentLocation = CGEventGetLocation(ourEvent); + CGEventRef ourEvent = CGEventCreate(NULL); + CGPoint currentLocation = CGEventGetLocation(ourEvent); CFRelease(ourEvent); if ([data isEqualToString:@""]) { @@ -104,7 +104,7 @@ -(void)performActionWithData:(NSString *)data verboseLoc = @"current location"; } else { NSArray *coords = [data componentsSeparatedByString:@","]; - + if ([coords count] != 2 || [[coords objectAtIndex:0] isEqualToString:@""] || [[coords objectAtIndex:1] isEqualToString:@""]) @@ -113,56 +113,64 @@ -(void)performActionWithData:(NSString *)data format:@"Invalid argument “%@” to command “%@”: Expected two coordinates (separated by a comma) or “.”. Examples: “%@:123,456” or “%@:.”", data, shortcut, shortcut, shortcut]; } - - p.x = [[self class] getCoordinate:[coords objectAtIndex:0] forAxis:XAXIS]; + + p.x = [[self class] getCoordinate:[coords objectAtIndex:0] forAxis:XAXIS]; p.y = [[self class] getCoordinate:[coords objectAtIndex:1] forAxis:YAXIS]; verboseLoc = [NSString stringWithFormat:@"%@,%@", [coords objectAtIndex:0], [coords objectAtIndex:1]]; } - - if (MODE_REGULAR != mode) { - printf("%s\n", [[self actionDescriptionString:verboseLoc] UTF8String]); + + if (MODE_REGULAR != options.mode) { + [options.verbosityOutputHandler write:[self actionDescriptionString:verboseLoc]]; } - - if (MODE_TEST == mode) { + + if (MODE_TEST == options.mode) { return; } - [self postHumanizedMouseEventsOfType:kCGEventMouseMoved toX:(float)p.x toY:(float)p.y]; + if (options.easing) { + // Eased move + [self postHumanizedMouseEventsWithEasingFactor:options.easing + toX:(float)p.x + toY:(float)p.y]; + } else { + // Move + CGEventRef move = CGEventCreateMouseEvent(NULL, [self getMoveEventConstant], CGPointMake(p.x, p.y), kCGMouseButtonLeft); // kCGMouseButtonLeft is ignored + CGEventPost(kCGHIDEventTap, move); + CFRelease(move); + } [self performActionAtPoint:p]; } --(void)postHumanizedMouseEventsOfType:(CGEventType)eventType - toX:(float)endX - toY:(float)endY { +- (uint32_t)getMoveEventConstant { + return kCGEventMouseMoved; +} + +- (void)postHumanizedMouseEventsWithEasingFactor:(unsigned)easing + toX:(float)endX + toY:(float)endY { - CGEventRef ourEvent = CGEventCreate(NULL); - CGPoint currentLocation = CGEventGetLocation(ourEvent); + CGEventRef ourEvent = CGEventCreate(NULL); + CGPoint currentLocation = CGEventGetLocation(ourEvent); CFRelease(ourEvent); + uint32_t eventConstant = [self getMoveEventConstant]; float startX = currentLocation.x; float startY = currentLocation.y; float distance = [self distanceBetweenPoint:currentLocation andPoint:NSMakePoint(endX, endY)]; - - unsigned steps = ((int)distance * 2) + 1; - DLog(@"Distance: %f / Steps: %i", distance, steps); + unsigned steps = ((int)(distance * easing / 100)) + 1; float xDiff = (endX - startX); float yDiff = (endY - startY); - - DLog(@"Move from %f/%f to %f/%f", currentLocation.x, currentLocation.y, endX, endY); - DLog(@"xDiff: %f, yDiff: %f", xDiff, yDiff); - float stepSize = 1.0 / (float)steps; - CGEventRef eventRef; for (unsigned i = 0; i < steps; i ++) { float factor = [self cubicEaseInOut:(stepSize * i)]; - eventRef = CGEventCreateMouseEvent(NULL, eventType, CGPointMake(startX + (factor * xDiff), startY + (factor * yDiff)), 0); + CGEventRef eventRef = CGEventCreateMouseEvent(NULL, eventConstant, CGPointMake(startX + (factor * xDiff), startY + (factor * yDiff)), 0); CGEventPost(kCGHIDEventTap, eventRef); + CFRelease(eventRef); usleep(220); } - CFRelease(eventRef); } - (float) distanceBetweenPoint:(NSPoint)a andPoint:(NSPoint)b { @@ -177,10 +185,10 @@ - (float) distanceBetweenPoint:(NSPoint)a andPoint:(NSPoint)b { // // Source: AHEasing, License: WTFPL // -// Expects the [whatever action] to be split up into small steps represented +// Expects [whatever action] to be split up into small steps represented // by a float from 0 (start) to 1 (end). Method is to be called with the float // and returns an "eased float" for it. --(float)cubicEaseInOut:(float)p { +- (float)cubicEaseInOut:(float)p { if (p < 0.5) { return 4 * p * p * p; } else { diff --git a/Actions/MoveAction.h b/Actions/MoveAction.h index afd9c53..af06032 100644 --- a/Actions/MoveAction.h +++ b/Actions/MoveAction.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -34,12 +34,12 @@ } -+(NSString *)commandShortcut; ++ (NSString *)commandShortcut; -+(NSString *)commandDescription; ++ (NSString *)commandDescription; --(NSString *)actionDescriptionString:(NSString *)locationDescription; +- (NSString *)actionDescriptionString:(NSString *)locationDescription; --(void)performActionAtPoint:(CGPoint) p; +- (void)performActionAtPoint:(CGPoint) p; @end diff --git a/Actions/MoveAction.m b/Actions/MoveAction.m index 732925f..23bb62b 100644 --- a/Actions/MoveAction.m +++ b/Actions/MoveAction.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -32,11 +32,11 @@ @implementation MoveAction #pragma mark - ActionProtocol -+(NSString *)commandShortcut { ++ (NSString *)commandShortcut { return @"m"; } -+(NSString *)commandDescription { ++ (NSString *)commandDescription { return @" m:x,y Will MOVE the mouse to the point with the given coordinates.\n" " Example: “m:12,34” will move the mouse to the point with\n" " x coordinate 12 and y coordinate 34."; @@ -44,12 +44,12 @@ +(NSString *)commandDescription { #pragma mark - MouseBaseAction --(NSString *)actionDescriptionString:(NSString *)locationDescription { +- (NSString *)actionDescriptionString:(NSString *)locationDescription { return [NSString stringWithFormat:@"Move to %@", locationDescription]; } --(void)performActionAtPoint:(CGPoint) p { - // Simply does nothing. Moving is done by the parent. +- (void)performActionAtPoint:(CGPoint) p { + // Simply does nothing. Moving is done by the parent. } @end diff --git a/Actions/PrintAction.h b/Actions/PrintAction.h index 1c9eeb6..27eee82 100644 --- a/Actions/PrintAction.h +++ b/Actions/PrintAction.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -33,11 +33,11 @@ } -+(NSString *)commandShortcut; ++ (NSString *)commandShortcut; -+(NSString *)commandDescription; ++ (NSString *)commandDescription; --(void)performActionWithData:(NSString *)data - inMode:(unsigned)mode; +- (void)performActionWithData:(NSString *)data + withOptions:(struct ExecutionOptions)options; @end diff --git a/Actions/PrintAction.m b/Actions/PrintAction.m index 0f7472f..0cc4112 100644 --- a/Actions/PrintAction.m +++ b/Actions/PrintAction.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -32,11 +32,11 @@ @implementation PrintAction #pragma mark - ActionProtocol -+(NSString *)commandShortcut { ++ (NSString *)commandShortcut { return @"p"; } -+(NSString *)commandDescription { ++ (NSString *)commandDescription { return @" p[:str] Will PRINT the given string. If the string is “.”, the current\n" " MOUSE POSITION is printed. As a convenience, you can skip the\n" " string completely and just write “p” to get the current position.\n" @@ -44,27 +44,27 @@ +(NSString *)commandDescription { " Example: “p:'Hello world'” will print “Hello world”"; } --(void)performActionWithData:(NSString *)data - inMode:(unsigned)mode { +- (void)performActionWithData:(NSString *)data + withOptions:(struct ExecutionOptions)options { if ([data isEqualToString:@""] || [data isEqualToString:@"."]) { - if (MODE_TEST == mode) { - printf("Print the current mouse position"); + if (MODE_TEST == options.mode) { + [options.verbosityOutputHandler write:@"Print the current mouse position"]; } else { CGEventRef ourEvent = CGEventCreate(NULL); - CGPoint ourLoc = CGEventGetLocation(ourEvent); - NSPoint point = NSPointFromCGPoint(ourLoc); - printf("Current mouse position: %.0f,%.0f\n", point.x, point.y); + CGPoint ourLoc = CGEventGetLocation(ourEvent); + NSPoint point = NSPointFromCGPoint(ourLoc); + [options.commandOutputHandler write:[NSString stringWithFormat: @"%.0f,%.0f", point.x, point.y]]; CFRelease(ourEvent); } return; } - - if (MODE_TEST == mode) { - printf("Print message “%s”\n", [data UTF8String]); + + if (MODE_TEST == options.mode) { + [options.verbosityOutputHandler write:[NSString stringWithFormat: @"Print message “%@”", data]]; } else { - printf("%s\n", [data UTF8String]); + [options.commandOutputHandler write:data]; } } diff --git a/Actions/RightClickAction.h b/Actions/RightClickAction.h index 1b63880..6a5c852 100644 --- a/Actions/RightClickAction.h +++ b/Actions/RightClickAction.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -34,12 +34,12 @@ } -+(NSString *)commandShortcut; ++ (NSString *)commandShortcut; -+(NSString *)commandDescription; ++ (NSString *)commandDescription; --(NSString *)actionDescriptionString:(NSString *)locationDescription; +- (NSString *)actionDescriptionString:(NSString *)locationDescription; --(void)performActionAtPoint:(CGPoint) p; +- (void)performActionAtPoint:(CGPoint) p; @end diff --git a/Actions/RightClickAction.m b/Actions/RightClickAction.m index f2178d6..761de16 100644 --- a/Actions/RightClickAction.m +++ b/Actions/RightClickAction.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -27,16 +27,17 @@ */ #import "RightClickAction.h" +#include @implementation RightClickAction #pragma mark - ActionProtocol -+(NSString *)commandShortcut { ++ (NSString *)commandShortcut { return @"rc"; } -+(NSString *)commandDescription { ++ (NSString *)commandDescription { return @" rc:x,y Will RIGHT-CLICK at the point with the given coordinates.\n" " Example: “rc:12,34” will right-click at the point with x coordinate\n" " 12 and y coordinate 34. Instead of x and y values, you may\n" @@ -46,16 +47,18 @@ +(NSString *)commandDescription { #pragma mark - MouseBaseAction --(NSString *)actionDescriptionString:(NSString *)locationDescription { +- (NSString *)actionDescriptionString:(NSString *)locationDescription { return [NSString stringWithFormat:@"Right-click at %@", locationDescription]; } --(void)performActionAtPoint:(CGPoint) p { +- (void)performActionAtPoint:(CGPoint) p { // Right button down CGEventRef rightDown = CGEventCreateMouseEvent(NULL, kCGEventRightMouseDown, CGPointMake(p.x, p.y), kCGMouseButtonRight); CGEventPost(kCGHIDEventTap, rightDown); CFRelease(rightDown); + usleep(15000); // Improve reliability + // Right button up CGEventRef rightUp = CGEventCreateMouseEvent(NULL, kCGEventRightMouseUp, CGPointMake(p.x, p.y), kCGMouseButtonRight); CGEventPost(kCGHIDEventTap, rightUp); diff --git a/Actions/TripleclickAction.h b/Actions/TripleclickAction.h index 91ef3f7..9084fb0 100644 --- a/Actions/TripleclickAction.h +++ b/Actions/TripleclickAction.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -34,10 +34,10 @@ } -+(NSString *)commandShortcut; ++ (NSString *)commandShortcut; --(NSString *)actionDescriptionString:(NSString *)locationDescription; +- (NSString *)actionDescriptionString:(NSString *)locationDescription; --(void)performActionAtPoint:(CGPoint) p; +- (void)performActionAtPoint:(CGPoint) p; @end diff --git a/Actions/TripleclickAction.m b/Actions/TripleclickAction.m index d10c86f..d32d81c 100644 --- a/Actions/TripleclickAction.m +++ b/Actions/TripleclickAction.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -33,11 +33,11 @@ @implementation TripleclickAction #pragma mark - ActionProtocol -+(NSString *)commandShortcut { ++ (NSString *)commandShortcut { return @"tc"; } -+(NSString *)commandDescription { ++ (NSString *)commandDescription { return @" tc:x,y Will TRIPLE-CLICK at the point with the given coordinates.\n" " Example: “tc:12,34” will triple-click at the point with x\n" " coordinate 12 and y coordinate 34. Instead of x and y values,\n" @@ -48,16 +48,16 @@ +(NSString *)commandDescription { #pragma mark - MouseBaseAction --(NSString *)actionDescriptionString:(NSString *)locationDescription { +- (NSString *)actionDescriptionString:(NSString *)locationDescription { return [NSString stringWithFormat:@"Triple-click at %@", locationDescription]; } --(void)performActionAtPoint:(CGPoint) p { - +- (void)performActionAtPoint:(CGPoint) p { + // Left button down CGEventRef mouseEvent = CGEventCreateMouseEvent(NULL, kCGEventLeftMouseDown, CGPointMake(p.x, p.y), kCGMouseButtonLeft); CGEventPost(kCGHIDEventTap, mouseEvent); - + // Left button up CGEventSetType(mouseEvent, kCGEventLeftMouseUp); CGEventPost(kCGHIDEventTap, mouseEvent); @@ -66,14 +66,14 @@ -(void)performActionAtPoint:(CGPoint) p { // 2nd/3rd click CGEventSetIntegerValueField(mouseEvent, kCGMouseEventClickState, 3); - - CGEventSetType(mouseEvent, kCGEventLeftMouseDown); - CGEventPost(kCGHIDEventTap, mouseEvent); - - CGEventSetType(mouseEvent, kCGEventLeftMouseUp); - CGEventPost(kCGHIDEventTap, mouseEvent); - - CFRelease(mouseEvent); + + CGEventSetType(mouseEvent, kCGEventLeftMouseDown); + CGEventPost(kCGHIDEventTap, mouseEvent); + + CGEventSetType(mouseEvent, kCGEventLeftMouseUp); + CGEventPost(kCGHIDEventTap, mouseEvent); + + CFRelease(mouseEvent); } @end diff --git a/Actions/TypeAction.h b/Actions/TypeAction.h index 35269f4..c41349f 100644 --- a/Actions/TypeAction.h +++ b/Actions/TypeAction.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/Actions/TypeAction.m b/Actions/TypeAction.m index de1a813..d97a2d0 100644 --- a/Actions/TypeAction.m +++ b/Actions/TypeAction.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -32,21 +32,20 @@ @implementation TypeAction #pragma mark - ActionProtocol -+(NSString *)commandShortcut { ++ (NSString *)commandShortcut { return @"t"; } -+(NSString *)commandDescription { ++ (NSString *)commandDescription { return @" t:text Will TYPE the given TEXT into the frontmost application.\n" " If the text includes space(s), it must be enclosed in quotes.\n" " Example: “type:Test” will type “Test” \n" " Example: “type:'Viele Grüße'” will type “Viele Grüße”"; } - #pragma mark - KeyBaseAction --(void)performActionWithKeycode:(CGKeyCode)code { +- (void)performActionWithKeycode:(CGKeyCode)code { CGEventRef e1 = CGEventCreateKeyboardEvent(NULL, (CGKeyCode)code, true); CGEventPost(kCGSessionEventTap, e1); CFRelease(e1); @@ -56,7 +55,8 @@ -(void)performActionWithKeycode:(CGKeyCode)code { CFRelease(e2); } --(void)performActionWithData:(NSString *)data inMode:(unsigned)mode { +- (void)performActionWithData:(NSString *)data + withOptions:(struct ExecutionOptions)options { struct timespec waitingtime; waitingtime.tv_sec = 0; @@ -70,9 +70,9 @@ -(void)performActionWithData:(NSString *)data inMode:(unsigned)mode { shortcut, shortcut, shortcut]; } - if (MODE_REGULAR != mode) { - printf("Type: “%s”\n", [data UTF8String]); - if (MODE_TEST == mode) { + if (MODE_REGULAR != options.mode) { + [options.verbosityOutputHandler write:[NSString stringWithFormat:@"Type: “%@”", data]]; + if (MODE_TEST == options.mode) { return; } } @@ -83,7 +83,7 @@ -(void)performActionWithData:(NSString *)data inMode:(unsigned)mode { NSArray *keyCodeInfos = [ki keyCodesForString:data]; NSUInteger j, jj; - + for (j = 0, jj = [keyCodeInfos count]; j < jj; ++j) { NSArray *keyCodeInfo = [keyCodeInfos objectAtIndex:j]; diff --git a/Actions/WaitAction.h b/Actions/WaitAction.h index c1f52c9..9e60d72 100644 --- a/Actions/WaitAction.h +++ b/Actions/WaitAction.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -33,9 +33,9 @@ } -+(NSString *)commandShortcut; ++ (NSString *)commandShortcut; --(void)performActionWithData:(NSString *)data - inMode:(unsigned)mode; +- (void)performActionWithData:(NSString *)data + withOptions:(struct ExecutionOptions)options; @end diff --git a/Actions/WaitAction.m b/Actions/WaitAction.m index a1fe54a..6cdcac7 100644 --- a/Actions/WaitAction.m +++ b/Actions/WaitAction.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -32,35 +32,35 @@ @implementation WaitAction #pragma mark - ActionProtocol -+(NSString *)commandShortcut { ++ (NSString *)commandShortcut { return @"w"; } -+(NSString *)commandDescription { ++ (NSString *)commandDescription { return @" w:ms Will WAIT/PAUSE for the given number of milliseconds.\n" " Example: “w:500” will pause command execution for half a second"; } --(void)performActionWithData:(NSString *)data - inMode:(unsigned)mode { +- (void)performActionWithData:(NSString *)data + withOptions:(struct ExecutionOptions)options { unsigned milliseconds = abs([data intValue]); NSString *shortcut = [[self class] commandShortcut]; - + if ([data isEqualToString:@""] || !milliseconds) { [NSException raise:@"InvalidCommandException" format:@"Invalid or missing argument to command “%@”: Expected number of milliseconds. Example: “%@:50”", shortcut, shortcut]; } - - if (MODE_REGULAR != mode) { - printf("Wait %i milliseconds\n", milliseconds); + + if (MODE_REGULAR != options.mode) { + [options.verbosityOutputHandler write:[NSString stringWithFormat:@"Wait %i milliseconds", milliseconds]]; } - - if (MODE_TEST == mode) { + + if (MODE_TEST == options.mode) { return; } - + struct timespec waitingtime; if (milliseconds > 999) { waitingtime.tv_sec = (int)floor(milliseconds / 1000); @@ -69,8 +69,8 @@ -(void)performActionWithData:(NSString *)data waitingtime.tv_sec = 0; waitingtime.tv_nsec = milliseconds * 1000000; } - - nanosleep(&waitingtime, NULL); + + nanosleep(&waitingtime, NULL); } @end diff --git a/ExecutionOptions.h b/ExecutionOptions.h new file mode 100644 index 0000000..1307de4 --- /dev/null +++ b/ExecutionOptions.h @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2007-2018, Carsten Blüm + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * - Neither the name of Carsten Blüm nor the names of his contributors may be + * used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import "OutputHandler.h" + +#ifndef ExecutionOptions_h +#define ExecutionOptions_h + +struct ExecutionOptions { + unsigned mode; + unsigned easing; + unsigned waitTime; + BOOL isFirstAction; + BOOL isLastAction; + OutputHandler *verbosityOutputHandler; + OutputHandler *commandOutputHandler; +}; + +#endif /* ExecutionOptions_h */ diff --git a/KeycodeInformer.h b/KeycodeInformer.h index d208a53..ccde7e4 100644 --- a/KeycodeInformer.h +++ b/KeycodeInformer.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -26,7 +26,6 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ - #import #include #include @@ -39,15 +38,18 @@ } + (id)sharedInstance; + - (NSArray *)keyCodesForString:(NSString *)string; + - (NSString *)stringForKeyCode:(CGKeyCode)keyCode andModifiers:(UInt32)modifiers; + - (NSString *)prepareString:(NSString *)string; /** - Returns a map of characters which require typing two characters with the current keyboard layout - - @warning This method is incomplete. It not only supports only a few keyboard layout, but also lacks lots of characters even for the few supported keyboard layouts. - @return NSDictionary which has the characters as keys and a string containing the characters to be typed as values + * Returns a map of characters which require typing two characters with the current keyboard layout + * + * @warning This method is incomplete. It not only supports only a few keyboard layout, but also lacks lots of characters even for the few supported keyboard layouts. + * @return NSDictionary which has the characters as keys and a string containing the characters to be typed as values */ - (NSDictionary *)getReplacementMapForKeyboardLayoutNamed:(NSString *)layoutName; diff --git a/KeycodeInformer.m b/KeycodeInformer.m index e4bb65b..1ad8611 100644 --- a/KeycodeInformer.m +++ b/KeycodeInformer.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -34,8 +34,9 @@ @implementation KeycodeInformer + (id)sharedInstance { @synchronized(self) { - if(sharedInstance == nil) + if (sharedInstance == nil) { sharedInstance = [[super allocWithZone:NULL] init]; + } } return sharedInstance; } @@ -47,12 +48,12 @@ + (id)allocWithZone:(NSZone *)zone { - (KeycodeInformer *)init { self = [super init]; if (self) { - keyboard = TISCopyCurrentKeyboardInputSource(); - keyLayoutData = TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData); + keyboard = TISCopyCurrentKeyboardInputSource(); + keyLayoutData = TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData); if (NULL == keyLayoutData) { keyboard = TISCopyCurrentASCIICapableKeyboardInputSource(); - keyLayoutData = TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData); + keyLayoutData = TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData); if (NULL == keyLayoutData) { [NSException raise:@"KeyboardLayoutException" format:@"Sorry, cliclick cannot handle your keyboard layout, so emulating typing is not possible."]; @@ -63,14 +64,13 @@ - (KeycodeInformer *)init { map = [[NSMutableDictionary alloc] initWithCapacity:256]; - // The N(123) macro is equivalent to @123, but is used for 10.6 compatibility - NSArray *keyCodes = [NSArray arrayWithObjects: N(0), N(1), N(2), N(3), N(4), N(5), N(6), N(7), N(8), N(9), - N(10), N(11), N(12), N(13), N(14), N(15), N(16), N(17), N(18), N(19), - N(20), N(21), N(22), N(23), N(24), N(25), N(26), N(27), N(28), N(29), - N(30), N(31), N(32), N(33), N(34), N(35), N(37), N(38), N(39), - N(40), N(41), N(42), N(43), N(44), N(45), N(46), N(47), N(49), - N(50), nil]; - + NSArray *keyCodes = [NSArray arrayWithObjects: @0, @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, @37, @38, @39, + @40, @41, @42, @43, @44, @45, @46, @47, @49, + @50, nil]; + for (NSNumber *keyCode in keyCodes) { NSString *string1 = [self stringForKeyCode:(CGKeyCode)[keyCode intValue] andModifiers:0]; [map setObject:[NSArray arrayWithObjects:keyCode, [NSNumber numberWithInt:0], nil] @@ -121,9 +121,9 @@ - (void)dealloc { - (NSArray *)keyCodesForString:(NSString *)string { NSMutableArray *keyCodes = [[NSMutableArray alloc] initWithCapacity:[string length]]; - string = [[self prepareString:string] decomposedStringWithCanonicalMapping]; + string = [[self prepareString:string] decomposedStringWithCanonicalMapping]; NSUInteger i, ii; - + for (i = 0, ii = [string length]; i < ii; i++) { NSRange range = [string rangeOfComposedCharacterSequenceAtIndex:i]; @@ -137,8 +137,8 @@ - (NSArray *)keyCodesForString:(NSString *)string { [keyCodes addObject:keyCodeInfo]; } else { NSFileHandle *fh = [NSFileHandle fileHandleWithStandardError]; - NSString *url = [NSString stringWithFormat:CHARINFO_URL_TEMPLATE, VERSION]; - NSString *msg = [NSString stringWithFormat:@"Unable to get key code for %@ (see %@)\n", [string substringWithRange:range], url]; + NSString *url = [NSString stringWithFormat:CHARINFO_URL_TEMPLATE, VERSION]; + NSString *msg = [NSString stringWithFormat:@"Unable to get key code for %@ (see %@)\n", [string substringWithRange:range], url]; [fh writeData:[msg dataUsingEncoding:NSUTF8StringEncoding]]; } } @@ -147,10 +147,10 @@ - (NSArray *)keyCodesForString:(NSString *)string { } - (NSString *)prepareString:(NSString *)string { - NSString *layoutName = TISGetInputSourceProperty(keyboard, kTISPropertyLocalizedName); + NSString *layoutName = TISGetInputSourceProperty(keyboard, kTISPropertyLocalizedName); NSDictionary *replacementMap = [self getReplacementMapForKeyboardLayoutNamed:layoutName]; - NSMutableString *tmp = [NSMutableString stringWithString:string]; - NSEnumerator *enumerator = [replacementMap keyEnumerator]; + NSMutableString *tmp = [NSMutableString stringWithString:string]; + NSEnumerator *enumerator = [replacementMap keyEnumerator]; NSString *key; DLog(@"Current keyboard layout: %@", layoutName); @@ -188,64 +188,62 @@ - (NSDictionary *)getReplacementMapForKeyboardLayoutNamed:(NSString *)layoutName // be typed by a combination of keys, but require consecutive key presses. Many // characters are missing, as I did not yet find a way to auto-generate the map. - // Note: The D(...) macro is equivalent to @{...}, but is used for 10.6 compatibility - if ([@"German" isEqualToString:layoutName]) { #pragma mark - German replacement map // #SUPPORTED German: ÄËÏÖÜŸäëïöüÿÁÉÍÓÚáéíóúÀÈÌÒÙàèìòùÂÊÎÔÛâêîôûÃÕÑãõñ // #KNOWN_UNSUPPORTED German: ŃńǸǹŇňŘřŠšŮů - return D( - // Umlauts - @"Ë", @"¨E", - @"Ÿ", @"¨Y", - @"ë", @"¨e", - @"ï", @"¨i", - @"ÿ", @"¨y", + return @{ + // U:lauts + @"Ë": @"¨E", + @"Ÿ": @"¨Y", + @"ë": @"¨e", + @"ï": @"¨i", + @"ÿ": @"¨y", // Acute - @"Á", @"´A", - @"É", @"´E", - @"Í", @"´I", - @"Ó", @"´O", - @"Ú", @"´U", - @"á", @"´a", - @"é", @"´e", - @"í", @"´i", - @"ó", @"´o", - @"ú", @"´u", + @"Á": @"´A", + @"É": @"´E", + @"Í": @"´I", + @"Ó": @"´O", + @"Ú": @"´U", + @"á": @"´a", + @"é": @"´e", + @"í": @"´i", + @"ó": @"´o", + @"ú": @"´u", // Agrave - @"À", @"`A", - @"È", @"`E", - @"Ì", @"`I", - @"Ò", @"`O", - @"Ù", @"`U", - @"à", @"`a", - @"è", @"`e", - @"ì", @"`i", - @"ò", @"`o", - @"ù", @"`u", - - // Circumflex - @"Â", @"^A", - @"Ê", @"^E", - @"Î", @"^I", - @"Ô", @"^O", - @"Û", @"^U", - @"â", @"^a", - @"ê", @"^e", - @"î", @"^i", - @"ô", @"^o", - @"û", @"^u", + @"À": @"`A", + @"È": @"`E", + @"Ì": @"`I", + @"Ò": @"`O", + @"Ù": @"`U", + @"à": @"`a", + @"è": @"`e", + @"ì": @"`i", + @"ò": @"`o", + @"ù": @"`u", + + // C:rcumflex + @"Â": @"^A", + @"Ê": @"^E", + @"Î": @"^I", + @"Ô": @"^O", + @"Û": @"^U", + @"â": @"^a", + @"ê": @"^e", + @"î": @"^i", + @"ô": @"^o", + @"û": @"^u", // Tilde - @"Ã", @"~A", - @"Ñ", @"~N", - @"Õ", @"~O", - @"ã", @"~a", - @"ñ", @"~n", - @"õ", @"~o" - ); + @"Ã": @"~A", + @"Ñ": @"~N", + @"Õ": @"~O", + @"ã": @"~a", + @"ñ": @"~n", + @"õ": @"~o" + }; } if ([@"U.S. Extended" isEqualToString:layoutName]) { @@ -253,363 +251,363 @@ - (NSDictionary *)getReplacementMapForKeyboardLayoutNamed:(NSString *)layoutName // #SUPPORTED U.S. Extended: ÄËÏÖÜŸäëïöüÿÁÉÍÓÚáéíóúÀÈÌÒÙàèìòùÂÊÎÔÛâêîôûÃÕÑãõñŃńǸǹŇňŘřŠšÅåŮů // #KNOWN_UNSUPPORTED U.S. Extended: // See http://symbolcodes.tlt.psu.edu/accents/codemacext.html - return D( + return @{ // Umlauts - @"Ä", @"¨A", - @"Ë", @"¨E", - @"Ï", @"¨I", - @"Ö", @"¨O", - @"Ü", @"¨U", - @"Ÿ", @"¨Y", - @"ä", @"¨a", - @"ë", @"¨e", - @"ï", @"¨i", - @"ö", @"¨o", - @"ü", @"¨u", - @"ÿ", @"¨y", + @"Ä": @"¨A", + @"Ë": @"¨E", + @"Ï": @"¨I", + @"Ö": @"¨O", + @"Ü": @"¨U", + @"Ÿ": @"¨Y", + @"ä": @"¨a", + @"ë": @"¨e", + @"ï": @"¨i", + @"ö": @"¨o", + @"ü": @"¨u", + @"ÿ": @"¨y", // Acute - @"Á", @"´A", - @"É", @"´E", - @"Í", @"´I", - @"Ń", @"´N", - @"Ó", @"´O", - @"Ú", @"´U", - @"á", @"´a", - @"é", @"´e", - @"í", @"´i", - @"ń", @"´n", - @"ó", @"´o", - @"ú", @"´u", + @"Á": @"´A", + @"É": @"´E", + @"Í": @"´I", + @"Ń": @"´N", + @"Ó": @"´O", + @"Ú": @"´U", + @"á": @"´a", + @"é": @"´e", + @"í": @"´i", + @"ń": @"´n", + @"ó": @"´o", + @"ú": @"´u", // Agrave - @"À", @"`A", - @"È", @"`E", - @"Ì", @"`I", - @"Ǹ", @"`N", - @"Ò", @"`O", - @"Ù", @"`U", - @"à", @"`a", - @"è", @"`e", - @"ì", @"`i", - @"ò", @"`o", - @"ù", @"`u", - @"ǹ", @"`n", + @"À": @"`A", + @"È": @"`E", + @"Ì": @"`I", + @"Ǹ": @"`N", + @"Ò": @"`O", + @"Ù": @"`U", + @"à": @"`a", + @"è": @"`e", + @"ì": @"`i", + @"ò": @"`o", + @"ù": @"`u", + @"ǹ": @"`n", // Circumflex - @"Â", @"ˆA", - @"Ê", @"ˆE", - @"Î", @"ˆI", - @"Ô", @"ˆO", - @"Û", @"ˆU", - @"â", @"ˆa", - @"ê", @"ˆe", - @"î", @"ˆi", - @"ô", @"ˆo", - @"û", @"ˆu", + @"Â": @"ˆA", + @"Ê": @"ˆE", + @"Î": @"ˆI", + @"Ô": @"ˆO", + @"Û": @"ˆU", + @"â": @"ˆa", + @"ê": @"ˆe", + @"î": @"ˆi", + @"ô": @"ˆo", + @"û": @"ˆu", // Tilde - @"Ã", @"˜A", - @"Ñ", @"˜N", - @"Õ", @"˜O", - @"ã", @"˜a", - @"ñ", @"˜n", - @"õ", @"˜o", + @"Ã": @"˜A", + @"Ñ": @"˜N", + @"Õ": @"˜O", + @"ã": @"˜a", + @"ñ": @"˜n", + @"õ": @"˜o", // Caron - @"Ň", @"ˇN", - @"Ř", @"ˇR", - @"Š", @"ˇS", - @"ň", @"ˇn", - @"ř", @"ˇr", - @"š", @"ˇs", + @"Ň": @"ˇN", + @"Ř": @"ˇR", + @"Š": @"ˇS", + @"ň": @"ˇn", + @"ř": @"ˇr", + @"š": @"ˇs", // A ring - @"Å", @"˚A", - @"å", @"˚a", - @"Ů", @"˚U", - @"ů", @"˚u" - ); + @"Å": @"˚A", + @"å": @"˚a", + @"Ů": @"˚U", + @"ů": @"˚u" + }; } if ([@"Polish" isEqualToString:layoutName]) { #pragma mark - Polish replacement map // #SUPPORTED Polish: ÄÖÜäöüÁÉÍÓÚáéíóúŃńŇňŘřŠš // #KNOWN_UNSUPPORTED Polish: ËÏŸëïÿÀÈÌÒÙàèìòùǸǹÂÊÎÔÛâêîôûÃÕÑãõñŒœÅåØøÆæ - return D( - // Umlauts - @"Ä", @"¨A", - @"Ö", @"¨O", - @"Ü", @"¨U", - @"ä", @"¨a", - @"ö", @"¨o", - @"ü", @"¨u", + return @{ + // U:lauts + @"Ä": @"¨A", + @"Ö": @"¨O", + @"Ü": @"¨U", + @"ä": @"¨a", + @"ö": @"¨o", + @"ü": @"¨u", // Acute - @"Á", @"´A", - @"É", @"´E", - @"Í", @"´I", - @"Ú", @"´U", - @"á", @"´a", - @"é", @"´e", - @"í", @"´i", - @"ú", @"´u", - - // Caron - @"Ň", @"ˇN", - @"Ř", @"ˇR", - @"Š", @"ˇS", - @"ň", @"ˇn", - @"ř", @"ˇr", - @"š", @"ˇs" - ); + @"Á": @"´A", + @"É": @"´E", + @"Í": @"´I", + @"Ú": @"´U", + @"á": @"´a", + @"é": @"´e", + @"í": @"´i", + @"ú": @"´u", + + // Caron + @"Ň": @"ˇN", + @"Ř": @"ˇR", + @"Š": @"ˇS", + @"ň": @"ˇn", + @"ř": @"ˇr", + @"š": @"ˇs" + }; } if ([@"French" isEqualToString:layoutName]) { #pragma mark - French replacement map // #SUPPORTED French: ÄËÏÖÜŸäëïöüÿÁÉÍÓÚáéíóúÀÈÌÒÙàèìòùÂÊÎÔÛâêîôû // #KNOWN_UNSUPPORTED French: ÃÑÕãñõŃńǸǹŇňŘřŠšŮů - return D( + return @{ // Umlauts - @"Ä", @"¨A", - @"Ë", @"¨E", - @"Ö", @"¨O", - @"Ü", @"¨U", - @"ä", @"¨a", - @"ë", @"¨e", - @"ö", @"¨o", - @"ü", @"¨u", - @"ÿ", @"¨y", + @"Ä": @"¨A", + @"Ë": @"¨E", + @"Ö": @"¨O", + @"Ü": @"¨U", + @"ä": @"¨a", + @"ë": @"¨e", + @"ö": @"¨o", + @"ü": @"¨u", + @"ÿ": @"¨y", // Acute - @"É", @"´E", - @"á", @"´a", - @"í", @"´i", - @"ó", @"´o", - @"ú", @"´u", + @"É": @"´E", + @"á": @"´a", + @"í": @"´i", + @"ó": @"´o", + @"ú": @"´u", // Agrave - @"À", @"`A", - @"ì", @"`i", - @"ò", @"`o", + @"À": @"`A", + @"ì": @"`i", + @"ò": @"`o", // Circumflex - @"â", @"^A", - @"û", @"^U", - @"Ǹ", @"`N", - @"ǹ", @"`n" - ); + @"â": @"^A", + @"û": @"^U", + @"Ǹ": @"`N", + @"ǹ": @"`n" + }; } if ([@"Canadian French - CSA" isEqualToString:layoutName]) { #pragma mark - Canadian French replacement map // #SUPPORTED Canadian French - CSA: ÄËÏÖÜŸäëïöüÿÁÉÍÓÚáéíóúÀÈÌÒÙàèìòùÂÊÎÔÛâêîôûÃÑÕãñõ // #KNOWN_UNSUPPORTED Canadian French - CSA: ŇŘŠňřšǸǹŃńÅåŮů - return D( + return @{ // Umlauts - @"Ä", @"¨A", - @"Ë", @"¨E", - @"Ï", @"¨I", - @"Ö", @"¨O", - @"Ü", @"¨U", - @"Ÿ", @"¨Y", - @"ä", @"¨a", - @"ë", @"¨e", - @"ï", @"¨i", - @"ö", @"¨o", - @"ü", @"¨u", - @"ÿ", @"¨y", + @"Ä": @"¨A", + @"Ë": @"¨E", + @"Ï": @"¨I", + @"Ö": @"¨O", + @"Ü": @"¨U", + @"Ÿ": @"¨Y", + @"ä": @"¨a", + @"ë": @"¨e", + @"ï": @"¨i", + @"ö": @"¨o", + @"ü": @"¨u", + @"ÿ": @"¨y", // Acute - @"Á", @"´A", - @"É", @"´E", - @"Í", @"´I", - @"Ó", @"´O", - @"Ú", @"´U", - @"á", @"´a", - @"í", @"´i", - @"ó", @"´o", - @"ú", @"´u", + @"Á": @"´A", + @"É": @"´E", + @"Í": @"´I", + @"Ó": @"´O", + @"Ú": @"´U", + @"á": @"´a", + @"í": @"´i", + @"ó": @"´o", + @"ú": @"´u", // Agrave - @"Ì", @"`I", - @"Ò", @"`O", - @"ì", @"`i", - @"ò", @"`o", - - // Circumflex - @"Â", @"^A", - @"Ê", @"^E", - @"Î", @"^I", - @"Ô", @"^O", - @"Û", @"^U", - @"â", @"^a", - @"ê", @"^e", - @"î", @"^i", - @"ô", @"^o", - @"û", @"^u", + @"Ì": @"`I", + @"Ò": @"`O", + @"ì": @"`i", + @"ò": @"`o", + + // C:rcumflex + @"Â": @"^A", + @"Ê": @"^E", + @"Î": @"^I", + @"Ô": @"^O", + @"Û": @"^U", + @"â": @"^a", + @"ê": @"^e", + @"î": @"^i", + @"ô": @"^o", + @"û": @"^u", // Tilde - @"Ã", @"~A", - @"Ñ", @"~N", - @"Õ", @"~O", - @"ã", @"~a", - @"ñ", @"~n", - @"õ", @"~o" - ); + @"Ã": @"~A", + @"Ñ": @"~N", + @"Õ": @"~O", + @"ã": @"~a", + @"ñ": @"~n", + @"õ": @"~o" + }; } if ([@"Spanish" isEqualToString:layoutName]) { #pragma mark - Spanish replacement map // #SUPPORTED Spanish: ÄËÏÖÜŸäëïöüÿÁÉÍÓÚáéíóúÀÈÌÒÙàèìòùÂÊÎÔÛâêîôûÑñ // #KNOWN_UNSUPPORTED Spanish: ÃÕãõŇŘŠňřšǸǹŃńŮů - return D( + return @{ // Umlauts - @"Ä", @"¨A", - @"Ë", @"¨E", - @"Ö", @"¨O", - @"Ü", @"¨U", - @"ä", @"¨a", - @"ë", @"¨e", - @"ï", @"¨i", - @"ö", @"¨o", - @"ü", @"¨u", - @"ÿ", @"¨y", + @"Ä": @"¨A", + @"Ë": @"¨E", + @"Ö": @"¨O", + @"Ü": @"¨U", + @"ä": @"¨a", + @"ë": @"¨e", + @"ï": @"¨i", + @"ö": @"¨o", + @"ü": @"¨u", + @"ÿ": @"¨y", // Acute - @"É", @"´E", - @"í", @"´i", - @"á", @"´a", - @"é", @"´e", - @"ó", @"´o", - @"ú", @"´u", + @"É": @"´E", + @"í": @"´i", + @"á": @"´a", + @"é": @"´e", + @"ó": @"´o", + @"ú": @"´u", // Agrave - @"À", @"`A", - @"à", @"`a", - @"è", @"`e", - @"ì", @"`i", - @"ò", @"`o", - @"ù", @"`u", + @"À": @"`A", + @"à": @"`a", + @"è": @"`e", + @"ì": @"`i", + @"ò": @"`o", + @"ù": @"`u", // Circumflex - @"â", @"^a", - @"ê", @"^e", - @"î", @"^i", - @"ô", @"^o", - @"û", @"^u" - ); + @"â": @"^a", + @"ê": @"^e", + @"î": @"^i", + @"ô": @"^o", + @"û": @"^u" + }; } if ([@"Portuguese" isEqualToString:layoutName]) { #pragma mark - Portuguese replacement map // #SUPPORTED Portuguese: ÄËÏÖÜäëïöüÿÁÉÍÓÚáéíóúÀÈÌÒÙàèìòùÂÊÎÔÛâêîôûÑñÃÕãõ // #KNOWN_UNSUPPORTED Portuguese: ŸŇŘŠňřšǸǹŃńŮů - return D( + return @{ // Umlauts - @"Ä", @"¨A", - @"Ë", @"¨E", - @"Ï", @"¨I", - @"Ö", @"¨O", - @"Ü", @"¨U", - @"ä", @"¨a", - @"ë", @"¨e", - @"ï", @"¨i", - @"ö", @"¨o", - @"ü", @"¨u", - @"ÿ", @"¨y", + @"Ä": @"¨A", + @"Ë": @"¨E", + @"Ï": @"¨I", + @"Ö": @"¨O", + @"Ü": @"¨U", + @"ä": @"¨a", + @"ë": @"¨e", + @"ï": @"¨i", + @"ö": @"¨o", + @"ü": @"¨u", + @"ÿ": @"¨y", // Acute - @"Á", @"´A", - @"É", @"´E", - @"Í", @"´I", - @"Ó", @"´O", - @"Ú", @"´U", - @"á", @"´a", - @"é", @"´e", - @"í", @"´i", - @"ó", @"´o", - @"ú", @"´u", + @"Á": @"´A", + @"É": @"´E", + @"Í": @"´I", + @"Ó": @"´O", + @"Ú": @"´U", + @"á": @"´a", + @"é": @"´e", + @"í": @"´i", + @"ó": @"´o", + @"ú": @"´u", // Agrave - @"À", @"`A", - @"È", @"`E", - @"Ì", @"`I", - @"Ò", @"`O", - @"Ù", @"`U", - @"à", @"`a", - @"è", @"`e", - @"ì", @"`i", - @"ò", @"`o", - @"ù", @"`u", + @"À": @"`A", + @"È": @"`E", + @"Ì": @"`I", + @"Ò": @"`O", + @"Ù": @"`U", + @"à": @"`a", + @"è": @"`e", + @"ì": @"`i", + @"ò": @"`o", + @"ù": @"`u", // Circumflex - @"Â", @"^A", - @"Ê", @"^E", - @"Î", @"^I", - @"Ô", @"^O", - @"Û", @"^U", - @"â", @"^a", - @"ê", @"^e", - @"î", @"^i", - @"ô", @"^o", - @"û", @"^u", + @"Â": @"^A", + @"Ê": @"^E", + @"Î": @"^I", + @"Ô": @"^O", + @"Û": @"^U", + @"â": @"^a", + @"ê": @"^e", + @"î": @"^i", + @"ô": @"^o", + @"û": @"^u", // Tilde - @"Ã", @"˜A", - @"Õ", @"˜O", - @"Ñ", @"˜N", - @"ã", @"˜a", - @"õ", @"˜o", - @"ñ", @"˜n" - ); + @"Ã": @"˜A", + @"Õ": @"˜O", + @"Ñ": @"˜N", + @"ã": @"˜a", + @"õ": @"˜o", + @"ñ": @"˜n" + }; } if ([@"Italian" isEqualToString:layoutName]) { #pragma mark - Italian replacement map // #SUPPORTED Italian: ÄËÏÖÜŸäëïöüÿÁÉÍÓÚáéíóúÀÈÌÒÙàèìòùÂÊÎÔÛâêîôûÑñÃÕãõ // #KNOWN_UNSUPPORTED Italian: ŸŇŘŠňřšǸǹŃńŮů - return D( + return @{ // Umlauts - @"Ä", @"¨A", - @"Ë", @"¨E", - @"Ï", @"¨I", - @"Ö", @"¨O", - @"Ü", @"¨U", - @"Ÿ", @"¨Y", - @"ä", @"¨a", - @"ë", @"¨e", - @"ï", @"¨i", - @"ö", @"¨o", - @"ü", @"¨u", - @"ÿ", @"¨y", + @"Ä": @"¨A", + @"Ë": @"¨E", + @"Ï": @"¨I", + @"Ö": @"¨O", + @"Ü": @"¨U", + @"Ÿ": @"¨Y", + @"ä": @"¨a", + @"ë": @"¨e", + @"ï": @"¨i", + @"ö": @"¨o", + @"ü": @"¨u", + @"ÿ": @"¨y", // Acute - @"á", @"´a", - @"í", @"´i", - @"ó", @"´o", - @"ú", @"´u", + @"á": @"´a", + @"í": @"´i", + @"ó": @"´o", + @"ú": @"´u", // Circumflex - @"Â", @"ˆA", - @"Ê", @"ˆE", - @"Î", @"ˆI", - @"Ô", @"ˆO", - @"Û", @"ˆU", - @"â", @"ˆa", - @"ê", @"ˆe", - @"î", @"ˆi", - @"ô", @"ˆo", - @"û", @"ˆu", - + @"Â": @"ˆA", + @"Ê": @"ˆE", + @"Î": @"ˆI", + @"Ô": @"ˆO", + @"Û": @"ˆU", + @"â": @"ˆa", + @"ê": @"ˆe", + @"î": @"ˆi", + @"ô": @"ˆo", + @"û": @"ˆu", + // Tilde - @"Ã", @"˜A", - @"Ñ", @"˜N", - @"Õ", @"˜O", - @"ã", @"˜a", - @"ñ", @"˜n", - @"õ", @"˜o" - ); + @"Ã": @"˜A", + @"Ñ": @"˜N", + @"Õ": @"˜O", + @"ã": @"˜a", + @"ñ": @"˜n", + @"õ": @"˜o" + }; } if ([@"Canadian English" isEqualToString:layoutName]) { diff --git a/Makefile b/Makefile index 0c6334b..7bb620a 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,7 @@ cliclick: Actions/ClickAction.o \ Actions/WaitAction.o \ ActionExecutor.o \ KeycodeInformer.o \ + OutputHandler.o \ cliclick.o gcc -o cliclick $^ -framework Cocoa -framework Carbon diff --git a/OutputHandler.h b/OutputHandler.h new file mode 100644 index 0000000..64f8864 --- /dev/null +++ b/OutputHandler.h @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2007-2018, Carsten Blüm + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * - Neither the name of Carsten Blüm nor the names of his contributors may be + * used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import + +@interface OutputHandler : NSObject { + NSString *outputTarget; +} + +- (id)initWithTarget:(NSString *)target; + +- (void)write:(NSString *)message; + +@end diff --git a/OutputHandler.m b/OutputHandler.m new file mode 100644 index 0000000..b3f7d42 --- /dev/null +++ b/OutputHandler.m @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2007-2018, Carsten Blüm + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * - Neither the name of Carsten Blüm nor the names of his contributors may be + * used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import "OutputHandler.h" + +@implementation OutputHandler + +- (id)initWithTarget:(NSString *)target { + self = [super init]; + + if (self) { + if (!target) { + outputTarget = @"stdout"; + } else if ([target isEqualToString:@"stdout"] || + [target isEqualToString:@"stderr"]) { + outputTarget = target; + } else if ([target isEqualToString:@"clipboard"]) { + outputTarget = target; + NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; + [pasteboard declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil]; + [pasteboard setString:@"" + forType:NSStringPboardType]; + } else { + NSFileManager *fm = [NSFileManager defaultManager]; + if ([fm fileExistsAtPath:target]) { + if (![fm isWritableFileAtPath:target]) { + [NSException raise:@"InvalidDestinationException" + format:@"Cannot write to the file “%@” specified as output destination.", target]; + } + } else { + if (![fm createFileAtPath:target contents:nil attributes:nil]) { + [NSException raise:@"InvalidDestinationException" + format:@"Cannot create file “%@” specified as output destination.", target]; + } + } + outputTarget = target; + } + } + return self; +} + +- (void)write:(NSString *)message { + if ([outputTarget isEqualToString:@"stdout"]) { + printf("%s\n", [message UTF8String]); + return; + } + + if ([outputTarget isEqualToString:@"stderr"]) { + fprintf(stderr, "%s\n", [message UTF8String]); + return; + } + + if ([outputTarget isEqualToString:@"clipboard"]) { + NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; + [pasteboard setString:[NSString stringWithFormat:@"%@%@\n", [pasteboard stringForType:NSStringPboardType], message] + forType:NSStringPboardType]; + return; + } + + // Still here? must be file target + NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:outputTarget]; + [fileHandle seekToEndOfFile]; + [fileHandle writeData:[[message stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]]; + [fileHandle closeFile]; +} + +@end diff --git a/Test.py b/Test.py index 3b602a3..91ea04b 100755 --- a/Test.py +++ b/Test.py @@ -1,130 +1,479 @@ #!/usr/bin/python -t # -*- coding: utf-8 -*- -# Tests command line parsing and output -# Expects to find an executable “cliclick” in /Build/Products/Debug/ - import sys import os.path +import tempfile +from subprocess import PIPE, Popen + +def assertOutput(testDescription, args, stdOutOutput, stdErrOutput): + cmd = "%s %s" % (sys.argv[1], args) + p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE) + stdout, stderr = p.communicate() + errors = [] + + if (stdOutOutput and -1 == stdout.find(stdOutOutput)): + errors.append("Did not find this string in stdout: %s (actual: %s)" % (stdOutOutput, stdout.replace("\n", " "))) + if (not stdOutOutput and stdout): + errors.append("Expected empty stdout, but got: %s" % (stdout.replace("\n", " "))) + + if (stdErrOutput and -1 == stderr.find(stdErrOutput)): + errors.append("Did not find this string in stderr: %s (actual: %s)" % (stdErrOutput, stderr.replace("\n", " "))) + if (not stdErrOutput and stderr): + errors.append("Expected empty stdout, but got: %s" % (stderr.replace("\n", " "))) + + if errors: + print "[ ] %s" % (testDescription) + for error in errors: + print " * %s" % (error) + print " * cliclick was called with argument(s): %s" % (args) + else: + print "[X] %s" % (testDescription) + + +def assertClipboardContains(testDescription, stdOutOutput): + p = Popen("pbpaste", shell=True, stdout=PIPE, stderr=PIPE) + stdout, stderr = p.communicate() + if (-1 == stdout.find(stdOutOutput)): + print "[ ] %s" % (testDescription) + else: + print "[X] %s" % (testDescription) + + +def assertFileContains(testDescription, filepath, needle): + f = open(filepath, 'r') + content = f.read() + if (-1 == content.find(needle)): + print "[ ] %s" % (testDescription) + else: + print "[X] %s" % (testDescription) -def runWithArguments(args = ""): - projdir = os.path.dirname(__file__) - executable = projdir + "/Build/Products/Debug/cliclick" - cmd = "%s -m test %s" % (executable, args) - result = ''.join(os.popen(cmd, 'r').readlines()) - return result -def expectStringForArguments(string, args): - result = runWithArguments(args) - if (-1 == result.find(string)): - print "Did not find [%s] in result when calling with arguments [%s].\nGot [%s]\n" % (string, args, result) +# Make sure cliclick path is passed as argument +if len(sys.argv) < 2: + print "Argument missing.\nUsage: %s /absolute/or/relative/path/to/cliclick" % (sys.argv[0]) + sys.exit(1) + # No arguments -expectStringForArguments("You did not pass any commands as argument", "") - -# Invalid action shortcut -expectStringForArguments("Unrecognized action shortcut “z”", "z") - -# Invalid X value -expectStringForArguments("Invalid X axis coordinate “foobar”", "m:foobar,-123") - -# Invalid Y value -expectStringForArguments("Invalid Y axis coordinate “foobar”", "m:+123,foobar") - -# “m” (move) -expectStringForArguments("Missing argument to command “m”: Expected", "m") -expectStringForArguments("Missing argument to command “m”: Expected", "m:") -expectStringForArguments("Invalid argument “123” to command “m”: Expected", "m:123") -expectStringForArguments("Invalid argument “123,” to command “m”: Expected", "m:123,") -expectStringForArguments("Move to 123,456", "m:123,456") -expectStringForArguments("Move to +20,-17", "m:+20,-17") - -# “w” (wait) -expectStringForArguments("Invalid or missing argument to command “w”: Expected", "w") -expectStringForArguments("Invalid or missing argument to command “w”: Expected", "w:") -expectStringForArguments("Invalid or missing argument to command “w”: Expected", "w:a") -expectStringForArguments("Wait 150 milliseconds", "w:150") -expectStringForArguments("Wait 2000 milliseconds", "w:2000") - -# “c” (click) -expectStringForArguments("Missing argument to command “c”: Expected", "c") -expectStringForArguments("Missing argument to command “c”: Expected", "c:") -expectStringForArguments("Invalid argument “1” to command “c”: Expected two coordinates", "c:1") -expectStringForArguments("Invalid argument “1,” to command “c”: Expected two coordinates", "c:1,") -expectStringForArguments("Click at 1129,64", "c:1129,64") -expectStringForArguments("Click at +0,80", "c:+0,80") -expectStringForArguments("Click at =-200,-100", "c:=-200,-100") - -# “dc” (double-click) -expectStringForArguments("Missing argument to command “dc”: Expected", "dc") -expectStringForArguments("Missing argument to command “dc”: Expected", "dc:") -expectStringForArguments("Invalid argument “1” to command “dc”: Expected two coordinates", "dc:1") -expectStringForArguments("Invalid argument “1,” to command “dc”: Expected two coordinates", "dc:1,") -expectStringForArguments("Double-click at 1129,64", "dc:1129,64") - -# “tc” (triple-click) -expectStringForArguments("Missing argument to command “tc”: Expected", "tc") -expectStringForArguments("Missing argument to command “tc”: Expected", "tc:") -expectStringForArguments("Invalid argument “1” to command “tc”: Expected two coordinates", "tc:1") -expectStringForArguments("Invalid argument “1,” to command “tc”: Expected two coordinates", "tc:1,") -expectStringForArguments("Triple-click at 1129,64", "tc:1129,64") - -# “rc” (right-click) -expectStringForArguments("Missing argument to command “rc”: Expected", "rc") -expectStringForArguments("Missing argument to command “rc”: Expected", "rc:") -expectStringForArguments("Invalid argument “1” to command “rc”: Expected two coordinates", "rc:1") -expectStringForArguments("Invalid argument “1,” to command “rc”: Expected two coordinates", "rc:1,") -expectStringForArguments("Right-click at 1129,64", "rc:1129,64") - -# “kd” (key down) -expectStringForArguments("Missing argument to command “kd”: Expected", "kd") -expectStringForArguments("Missing argument to command “kd”: Expected", "kd:") -expectStringForArguments("Invalid key “abc” given as argument to command “kd”", "kd:abc") -expectStringForArguments("Invalid key “return” given as argument to command “kd”", "kd:return") -expectStringForArguments("Hold ctrl key down", "kd:ctrl") -expectStringForArguments("Hold ctrl key down\nHold cmd key down\nHold alt key down", "kd:ctrl,cmd,alt") -expectStringForArguments("Invalid key “return” given as argument", "kd:return") - -# “ku” (key up) -expectStringForArguments("Missing argument to command “ku”: Expected", "ku") -expectStringForArguments("Missing argument to command “ku”: Expected", "ku:") -expectStringForArguments("Invalid key “abc” given as argument to command “ku”", "ku:abc") -expectStringForArguments("Release ctrl key", "ku:ctrl") -expectStringForArguments("Release ctrl key\nRelease cmd key\nRelease alt key", "ku:ctrl,cmd,alt") - -# “kp” (key press) -expectStringForArguments("Missing argument to command “kp”: Expected", "kp") -expectStringForArguments("Missing argument to command “kp”: Expected", "kp:") -expectStringForArguments("Invalid key “abc” given as argument to command “kp”", "kp:abc") -expectStringForArguments("Invalid key “cmd” given as argument to command “kp”", "kp:cmd") -expectStringForArguments("Press + release return key", "kp:return") - -# “p” (print) -expectStringForArguments("Print the current mouse position", "p") -expectStringForArguments("Print the current mouse position", "p:.") -expectStringForArguments("Print the current mouse position", "p:'.'") -expectStringForArguments("Print message “Hello world”", "p:'Hello world'") - -# “dd” (drag down) -expectStringForArguments("Missing argument to command “dd”: Expected", "dd") -expectStringForArguments("Missing argument to command “dd”: Expected", "dd:") -expectStringForArguments("Invalid argument “1” to command “dd”: Expected two coordinates", "dd:1") -expectStringForArguments("Invalid argument “1,” to command “dd”: Expected two coordinates", "dd:1,") -expectStringForArguments("Drag press down at 1129,64", "dd:1129,64") - -# “du” (drag up) -expectStringForArguments("Missing argument to command “du”: Expected", "du") -expectStringForArguments("Missing argument to command “du”: Expected", "du:") -expectStringForArguments("Invalid argument “1” to command “du”: Expected two coordinates", "du:1") -expectStringForArguments("Invalid argument “1,” to command “du”: Expected two coordinates", "du:1,") -expectStringForArguments("Drag release at 1129,64", "du:1129,64") - -# “t” (type) -expectStringForArguments("Missing argument to command “t”: Expected", "t") -expectStringForArguments("Missing argument to command “t”: Expected", "t:") -expectStringForArguments("Type: “Type this: How are you today?”", "t:'Type this: How are you today?'") - -# “cp” (color picker) -expectStringForArguments("Missing argument to command “cp”: Expected", "cp") -expectStringForArguments("Missing argument to command “cp”: Expected", "cp:") -expectStringForArguments("Print color at location 123,456", "cp:123,456") +assertOutput( + "When called without argument, should write nothing to stdout and usage info to stderr", + "", + "", + "You did not pass any commands as argument" +) + +assertOutput( + "When called with an unknown action, should write nothing to stdout and error to stderr", + "z", + "", + "Unrecognized action shortcut “z”" +) + + +# Assertions for move / m command +assertOutput( + "When using “move” (“m”) command without coordinates, should write nothing to stdout and error to stderr", + "m", + "", + "Missing argument to command “m”" +) + +assertOutput( + "When using “move” (“m”) command with colon, but without coordinates, should write nothing to stdout and error to stderr", + "m", + "", + "Missing argument to command “m”" +) + +assertOutput( + "When using “move” (“m”) command with invalid X value, should write nothing to stdout and error to stderr", + "m:foobar,-123", + "", + "Invalid X axis coordinate “foobar”" +) + +assertOutput( + "When using “move” (“m”) command with invalid Y value, should write nothing to stdout and error to stderr", + "m:+123,xxx", + "", + "Invalid Y axis coordinate “xxx”" +) + +assertOutput( + "When using “move” (“m”) command with missing Y value, should write nothing to stdout and error to stderr", + "m:+123,", + "", + "Invalid argument “+123,” to command “m”" +) + +assertOutput( + "When using “move” (“m”) in testing mode, should write action to stdout and nothing to stderr", + "-m test m:123,456", + "Move to 123,456", + "" +) + +assertOutput( + "When using “move” (“m”) in testing mode with relative coordinates, should write action to stdout and nothing to stderr", + "-m test m:+20,-17", + "Move to +20,-17", + "" +) + +# Assertions for click / c command +assertOutput( + "When using “click” (“c”) command without coordinates, should write nothing to stdout and error to stderr", + "c", + "", + "Missing argument to command “c”" +) + +assertOutput( + "When using “click” (“c”) command with colon, but without coordinates, should write nothing to stdout and error to stderr", + "c", + "", + "Missing argument to command “c”" +) + +assertOutput( + "When using “click” (“c”) command with invalid X value, should write nothing to stdout and error to stderr", + "c:foobar,-123", + "", + "Invalid X axis coordinate “foobar”" +) + +assertOutput( + "When using “click” (“c”) command with invalid Y value, should write nothing to stdout and error to stderr", + "c:+123,xxx", + "", + "Invalid Y axis coordinate “xxx”" +) + +assertOutput( + "When using “click” (“c”) command with missing Y value, should write nothing to stdout and error to stderr", + "c:+123,", + "", + "Invalid argument “+123,” to command “c”" +) + +assertOutput( + "When using “click” (“c”) in testing mode, should write action to stdout and nothing to stderr", + "-m test c:123,456", + "Click at 123,456", + "" +) + +assertOutput( + "When using “click” (“c”) in testing mode with relative coordinates, should write action to stdout and nothing to stderr", + "-m test c:+20,-17", + "Click at +20,-17", + "" +) + +assertOutput( + "When using “click” (“c”) in testing mode with absolute negative coordinates, should write action to stdout and nothing to stderr", + "-m test c:=-200,-100", + "Click at =-200,-100", + "" +) + + +# Assertions for double-click / command +assertOutput( + "When using “doubleclick” (“dc”) command without coordinates, should write nothing to stdout and error to stderr", + "dc", + "", + "Missing argument to command “dc”" +) + +assertOutput( + "When using “doubleclick” (“dc”) in testing mode, should write action to stdout and nothing to stderr", + "-m test dc:1129,64", + "Double-click at 1129,64", + "" +) + +# Assertions for triple-click / command +assertOutput( + "When using “tripleclick” (“tc”) command without coordinates, should write nothing to stdout and error to stderr", + "tc", + "", + "Missing argument to command “tc”" +) + +assertOutput( + "When using “tripleclick” (“tc”) in testing mode, should write action to stdout and nothing to stderr", + "-m test tc:1129,64", + "Triple-click at 1129,64", + "" +) + +# Assertions for right-click / command +assertOutput( + "When using “rightclick” (“rc”) command without coordinates, should write nothing to stdout and error to stderr", + "rc", + "", + "Missing argument to command “rc”" +) + +assertOutput( + "When using “rightclick” (“rc”) in testing mode, should write action to stdout and nothing to stderr", + "-m test rc:1129,64", + "Right-click at 1129,64", + "" +) + +# Assertions for drag down / dd command +assertOutput( + "When using “drag down” (“dd) command without coordinates, should write nothing to stdout and error to stderr", + "dd", + "", + "Missing argument to command “dd”" +) + +assertOutput( + "When using “drag down” (“dd”) in testing mode, should write action to stdout and nothing to stderr", + "-m test dd:1129,64", + "Drag press down at 1129,64", + "" +) + +# Assertions for drag up / du command +assertOutput( + "When using “drag up” (“du) command without coordinates, should write nothing to stdout and error to stderr", + "du:", + "", + "Missing argument to command “du”" +) + +assertOutput( + "When using “drag up” (“rc”) in testing mode, should write action to stdout and nothing to stderr", + "-m test du:1129,64", + "Drag release at 1129,64", + "" +) + +# Assertions for wait / w command +assertOutput( + "When using “wait” (“w”) command without argument, should write nothing to stdout and error to stderr", + "w", + "", + "Invalid or missing argument to command “w”" +) + +assertOutput( + "When using “wait” (“w”) command with a non-numeric argument, should write nothing to stdout and error to stderr", + "w:abc", + "", + "Invalid or missing argument to command “w”" +) + +assertOutput( + "When using “wait” (“w”) command in testing mode, should write the action to stdout and nothing to stderr", + "-m test w:150", + "Wait 150 milliseconds", + "" +) + +# Assertions for key down / kd command +assertOutput( + "When using “key down” (“kd”) command without argument, should write nothing to stdout and error to stderr", + "kd", + "", + "Missing argument to command “kd”", +) + +assertOutput( + "When using “key down” (“kd”) with an invalid key, should write nothing to stdout and error to stderr", + "kd:abc", + "", + "Invalid key “abc” given as argument to command “kd”", +) + +assertOutput( + "When using “key down” (“kd”) with a valid key, should write the action stdout and nothing to stderr", + "-m test kd:ctrl", + "Hold ctrl key down", + "", +) + +assertOutput( + "When using “key down” (“kd”) with several valid keys, should write the action stdout and nothing to stderr", + "-m test kd:ctrl,cmd,alt", + "Hold ctrl key down\nHold cmd key down\nHold alt key down", + "", +) + +# Assertions for key up / ku command +assertOutput( + "When using “key up” (“ku”) command without argument, should write nothing to stdout and error to stderr", + "ku", + "", + "Missing argument to command “ku”", +) + +assertOutput( + "When using “key up” (“ku”) with an invalid key, should write nothing to stdout and error to stderr", + "ku:abc", + "", + "Invalid key “abc” given as argument to command “ku”", +) + +assertOutput( + "When using “key up” (“ku”) with a valid key, should write the action to stdout and nothing to stderr", + "-m test ku:ctrl", + "Release ctrl key", + "", +) + +assertOutput( + "When using “key up” (“ku”) with several valid keys, should write the action stdout and nothing to stderr", + "-m test ku:ctrl,cmd,alt", + "Release ctrl key\nRelease cmd key\nRelease alt key", + "", +) + +# Assertions for key press / kp command +assertOutput( + "When using “key press” (“kp”) command without argument, should write nothing to stdout and error to stderr", + "kp", + "", + "Missing argument to command “kp”" +) + +assertOutput( + "When using “key press” (“kp”) command with an invalid key, should write nothing to stdout and error to stderr", + "kp:abc", + "", + "Invalid key “abc” given as argument to command “kp”" +) + +assertOutput( + "When using “key press” (“kp”) command with a valid key, should write the action to stdout and nothing to stderr", + "-m test kp:return", + "Press + release return key", + "" +) + +# Assertions for print / p command +assertOutput( + "When using “print” (“p”) command without argument, should print the current mouse position", + "-m test p", + "Print the current mouse position", + "" +) + +assertOutput( + "When using “print” (“p”) command with a dot as argument, should print the current mouse position", + "-m test p:.", + "Print the current mouse position", + "" +) + +assertOutput( + "When using “print” (“p”) command with a string, should print the string", + "-m test p:'Hello world'", + "Print message “Hello world”", + "" +) + +# Assertions for type / t command +assertOutput( + "When using “type” (“t”) command without argument, should write nothing to stdout and error to stderr", + "t", + "", + "Missing argument to command “t”" +) + +assertOutput( + "When using “type” (“t”) command with a string as argument, should write the action to stdout and nothing to stderr", + "-m test t:'Type this: How are you today?'", + "Type: “Type this: How are you today?”", + "" +) + +# Assertions for color picker / cp command +assertOutput( + "When using “color picker (“cp”) command without argument, should write nothing to stdout and error to stderr", + "cp", + "", + "Missing argument to command “cp”" +) + +assertOutput( + "When using “color picker (“cp”) command with invalid coordinates, should write nothing to stdout and error to stderr", + "cp:123", + "", + "Invalid argument “123” to command “cp”" +) + +assertOutput( + "When using color” (“cp”) command with a dot as argument, should write the action to stdout and nothing to stderr", + "-m test cp:.", + "Print color at current mouse position", + "" +) + +assertOutput( + "When using color” (“cp”) command with coordinates as argument, should write the action to stdout and nothing to stderr", + "-m test cp:123,456", + "Print color at location 123,456", + "" +) + +assertOutput( + "When setting the output destination for test mode to stderr, should write nothing to stdout and the action to stderr", + "-m test:stderr c:. m:123,456", + "", + "Click at current location\nMove to 123,456" +) + +# Test writing verbose messages to a file +assertOutput( + "When setting the output destination for test mode to a file with an invalid path, should write nothing to stdout and the error to stderr", + "-m test:/no/such/path.txt c:. m:123,456", + "", + "Cannot create file “/no/such/path.txt” specified as output destination" +) + +tempfilePath = tempfile.mkstemp()[1] +assertOutput( + "When setting the output destination for test mode to a file, should write nothing to stdout and nothing to stderr", + "-m test:%s c:. m:123,456" % (tempfilePath), + "", + "" +) +assertFileContains( + "When setting the output destination for test mode to a file, the file should contain the commands", + tempfilePath, + "Click at current location\nMove to 123,456" +) + +# Test writing verbose messages to the clipboard +assertOutput( + "When setting the output destination for test mode to the clipboard, should write nothing to stdout and nothing to stderr", + "-m test:clipboard c:. m:123,456", + "", + "" +) +assertClipboardContains( + "When setting the output destination for test mode to the clipboard, the clipboard should contain the commands", + "Click at current location\nMove to 123,456" +) + +# Test setting command output destination using -d +assertOutput( + "When setting the destination for command output to stderr, should write nothing to stdout and the action to stderr", + "-d stderr p:OK", + "", + "OK" +) + +# Test setting command output destination using -d +assertOutput( + "When setting the destination for command output to stdout, should write the action to stdout and nothing to stderr", + "-d stdout p:OK", + "OK", + "" +) diff --git a/cliclick.m b/cliclick.m index 72b6bf7..f0eac7d 100644 --- a/cliclick.m +++ b/cliclick.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2007-2015, Carsten Blüm + * Copyright (c) 2007-2018, Carsten Blüm * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -28,29 +28,37 @@ #include #include +#include #include #import #import "ActionExecutor.h" #import "MoveAction.h" +#import "OutputHandler.h" +#import "ExecutionOptions.h" + +void error(void); +void help(void); -void error(); -void help(); NSArray* parseCommandsFile(NSString *filepath); int main (int argc, const char * argv[]) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; - NSString *modeOption = nil; + struct ExecutionOptions executionOptions; + executionOptions.easing = 0; + executionOptions.waitTime = 0; + executionOptions.mode = MODE_REGULAR; + NSArray *modeOptionArg; + NSString *verbosityOutputDestination = nil; NSString *filepath = nil; + NSString *commandOutputDestination = nil; NSArray *actions; CGPoint initialMousePosition; BOOL restoreOption = NO; - unsigned mode; - unsigned waitTime = 0; int optchar; - while ((optchar = getopt(argc, (char * const *)argv, "hoVm:rf:w:n")) != -1) { + while ((optchar = getopt(argc, (char * const *)argv, "horVne:f:d:m:w:")) != -1) { switch(optchar) { case 'h': help(); @@ -69,16 +77,34 @@ int main (int argc, const char * argv[]) { [pool release]; return EXIT_SUCCESS; case 'm': - modeOption = [NSString stringWithCString:optarg encoding:NSASCIIStringEncoding]; + modeOptionArg = [[NSString stringWithCString:optarg encoding:NSASCIIStringEncoding] componentsSeparatedByString:@":"]; + if ([[modeOptionArg objectAtIndex:0] isEqualToString:@"verbose"]) { + executionOptions.mode = MODE_VERBOSE; + } else if ([[modeOptionArg objectAtIndex:0] isEqualToString:@"test"]) { + executionOptions.mode = MODE_TEST; + } else { + fprintf(stderr, "Only “verbose” or “test” are valid values for the -m argument\n"); + [pool release]; + return EXIT_FAILURE; + } + if ([modeOptionArg count] > 1 && [modeOptionArg objectAtIndex:1]) { + verbosityOutputDestination = [modeOptionArg objectAtIndex:1]; + } + break; + case 'e': + executionOptions.easing = atoi(optarg) > 0 ? atoi(optarg) : 0; break; case 'f': filepath = [NSString stringWithCString:optarg encoding:NSASCIIStringEncoding]; break; + case 'd': + commandOutputDestination = [NSString stringWithCString:optarg encoding:NSASCIIStringEncoding]; + break; case 'r': restoreOption = YES; break; case 'w': - waitTime = atoi(optarg); + executionOptions.waitTime = atoi(optarg) > 0 ? atoi(optarg) : 0; break; default: [pool release]; @@ -86,14 +112,12 @@ int main (int argc, const char * argv[]) { } } - if ([modeOption isEqualToString:@"verbose"]) { - mode = MODE_VERBOSE; - } else if ([modeOption isEqualToString:@"test"]) { - mode = MODE_TEST; - } else if (!modeOption) { - mode = MODE_REGULAR; - } else { - printf("Only “verbose” or “test” are valid values for the -m argument\n"); + @try { + executionOptions.commandOutputHandler = [[OutputHandler alloc] initWithTarget:commandOutputDestination]; + executionOptions.verbosityOutputHandler = [[OutputHandler alloc] initWithTarget:verbosityOutputDestination]; + } + @catch (NSException *e) { + fprintf(stderr, "%s\n", [[e reason] UTF8String]); [pool release]; return EXIT_FAILURE; } @@ -109,19 +133,19 @@ int main (int argc, const char * argv[]) { return EXIT_FAILURE; } - if (mode == MODE_TEST) { - printf("Running in test mode. These command(s) would be executed:\n"); + if (executionOptions.mode == MODE_TEST) { + [executionOptions.verbosityOutputHandler write:@"Running in test mode. These command(s) would be executed:"]; } if (filepath) { NSFileManager *fm = [NSFileManager defaultManager]; if ([filepath isEqualToString:@""]) { - printf("Option -f expects a path: -f /path/to/the/file\n"); + fprintf(stderr, "Option -f expects a path: -f /path/to/the/file\n"); [pool release]; return EXIT_FAILURE; } if (![filepath isEqualToString:@"-"] && ![fm fileExistsAtPath:filepath]) { - printf("There is no file at %s\n", [filepath UTF8String]); + fprintf(stderr, "There is no file at %s\n", [filepath UTF8String]); [pool release]; return EXIT_FAILURE; } @@ -132,28 +156,20 @@ int main (int argc, const char * argv[]) { } @try { - [ActionExecutor executeActions:actions - inMode:mode - waitingMilliseconds:waitTime]; + [ActionExecutor executeActions:actions withOptions:executionOptions]; } @catch (NSException *e) { - printf("%s\n", [[e reason] UTF8String]); + fprintf(stderr, "%s\n", [[e reason] UTF8String]); [pool release]; return EXIT_FAILURE; } - @finally { - // Nothing to clean up - } if (restoreOption) { NSString *positionString = [NSString stringWithFormat:@"%d,%d", (int)initialMousePosition.x, (int)initialMousePosition.y]; id moveAction = [[MoveAction alloc] init]; [moveAction performActionWithData:positionString - inMode:MODE_REGULAR]; + withOptions:executionOptions]; [moveAction release]; - if (mode == MODE_VERBOSE) { - printf("Restoring mouse position to %s\n", [positionString UTF8String]); - } } [pool release]; @@ -169,6 +185,7 @@ int main (int argc, const char * argv[]) { NSData *stdinData = [[NSFileHandle fileHandleWithStandardInput] readDataToEndOfFile]; NSString *configString = [[NSString alloc] initWithData:stdinData encoding:NSUTF8StringEncoding]; lines = [configString componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]; + [configString release]; } else { // File NSString *fileContents = [NSString stringWithContentsOfFile:filepath @@ -193,8 +210,8 @@ int main (int argc, const char * argv[]) { } void error() { - printf("You did not pass any commands as argument to cliclick.\n"); - printf("Call cliclick with option -h to see usage instructions.\n"); + fprintf(stderr, "You did not pass any commands as argument to cliclick.\n"); + fprintf(stderr, "Call cliclick with option -h to see usage instructions.\n"); } void help() { @@ -209,28 +226,39 @@ void help() { " cliclick [-m ] [-f ] [-w ] [-r] command1 [command2] [...]\n" "\n" "OPTIONS\n" - " -r Restore initial mouse location when finished\n" - " -m The mode can be either “verbose” (cliclick will print a\n" - " description of each action to stdout just before it is\n" - " performed) or “test” (cliclick will only print the\n" - " description, but not perform the action)\n" - " -f Instead of passing commands as arguments, you may instead\n" - " specify a file from which cliclick will read the commands\n" - " (or stdin, when - is given as filename).\n" - " Each line in the file is expected to contain a command\n" - " in the same format/syntax as commands given as arguments\n" - " at the shell. Additionally, lines starting with the hash\n" - " character # are regarded as comments, i.e.: ignored. Leading\n" - " and trailing whitespace is ignored, too.\n" - " -w Wait the given number of milliseconds after each event.\n" - " If you find that you use the “wait” command too often,\n" - " using -w could make things easier. Please note that “wait”\n" - " is not affected by -w. This means that invoking\n" - " “cliclick -w 200 wait:500” will wait for 700 milliseconds.\n" - " The default (and minimum) value for -w is 20.\n" - " -V Show cliclick version number and release date\n" - " -o Open version history in a browser\n" - " -n Send a donation\n" + " -r Restore initial mouse location when finished\n" + " -m The mode can be either “verbose” (cliclick will print a\n" + " description of each action to stdout just before it is\n" + " performed) or “test” (cliclick will only print the\n" + " description, but not perform the action)\n" + " -d Specify the target when using the “p” (“print”) command.\n" + " Possible values are: stdout, stderr, clipboard or the path \n" + " to a file (which will be overwritten if it exists).\n" + " By default (if option not given), stdout is used for printing\n" + " -e Set an easing factor for mouse movements. The higher this\n" + " value is (default: 0), the more will mouse movements seem\n" + " “natural” or “human-like”, which also implies: will be slower.\n" + " If this option is used, the actual speed will also depend\n" + " on the distance between the start and the end position, i.e.\n" + " the time needed for moving will be higher if the distance\n" + " is larger.\n" + " -f Instead of passing commands as arguments, you may instead\n" + " specify a file from which cliclick will read the commands\n" + " (or stdin, when - is given as filename).\n" + " Each line in the file is expected to contain a command\n" + " in the same format/syntax as commands given as arguments\n" + " at the shell. Additionally, lines starting with the hash\n" + " character # are regarded as comments, i.e.: ignored. Leading\n" + " and trailing whitespace is ignored, too.\n" + " -w Wait the given number of milliseconds after each event.\n" + " If you find that you use the “wait” command too often,\n" + " using -w could make things easier. Please note that “wait”\n" + " is not affected by -w. This means that invoking\n" + " “cliclick -w 200 wait:500” will wait for 700 milliseconds.\n" + " The default (and minimum) value for -w is 20.\n" + " -V Show cliclick version number and release date\n" + " -o Open version history in a browser\n" + " -n Send a donation\n" "\n" "COMMANDS\n" "To use cliclick, you pass an arbitrary number of commands as arguments. A command consists of a " @@ -256,7 +284,7 @@ void help() { NSString *author = [NSString stringWithFormat:@"Version %@, released %@\n" "Author: Carsten Blüm, \n" "List of contributors: https://github.com/BlueM/cliclick/graphs/contributors\n" - "Website: www.bluem.net/jump/cliclick/\n\n", + "Website: https://www.bluem.net/jump/cliclick/\n\n", VERSION, RELEASEDATE]; printf("%s", [author UTF8String]); diff --git a/cliclick.xcodeproj/project.pbxproj b/cliclick.xcodeproj/project.pbxproj index b2fc2c5..f71f919 100644 --- a/cliclick.xcodeproj/project.pbxproj +++ b/cliclick.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 2250A92A1966D39000150E07 /* KeycodeInformer.m in Sources */ = {isa = PBXBuildFile; fileRef = 2250A9291966D39000150E07 /* KeycodeInformer.m */; }; 226347791B998B80003C5D71 /* ColorPickerAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 226347781B998B80003C5D71 /* ColorPickerAction.m */; }; 226A924F16AC9D7B0079165A /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 226A924E16AC9D7B0079165A /* Cocoa.framework */; }; + 22760C932052632600467D47 /* OutputHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 22760C922052632600467D47 /* OutputHandler.m */; }; 228B7AA81B54179A003D6960 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 228B7AA71B54179A003D6960 /* IOKit.framework */; }; 22998C7D197A85A300A4C691 /* TypeAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 22998C7C197A85A300A4C691 /* TypeAction.m */; }; 22B4D99C15D6CB0A002C8478 /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22B4D99B15D6CB0A002C8478 /* Carbon.framework */; }; @@ -57,6 +58,9 @@ 226347771B998B80003C5D71 /* ColorPickerAction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ColorPickerAction.h; path = Actions/ColorPickerAction.h; sourceTree = ""; }; 226347781B998B80003C5D71 /* ColorPickerAction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ColorPickerAction.m; path = Actions/ColorPickerAction.m; sourceTree = ""; }; 226A924E16AC9D7B0079165A /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; + 22760C912052632600467D47 /* OutputHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OutputHandler.h; sourceTree = ""; }; + 22760C922052632600467D47 /* OutputHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OutputHandler.m; sourceTree = ""; }; + 22760C9420526A5000467D47 /* ExecutionOptions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ExecutionOptions.h; sourceTree = ""; }; 228B7AA71B54179A003D6960 /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = ../../../../../../System/Library/Frameworks/IOKit.framework; sourceTree = ""; }; 22998C7B197A85A300A4C691 /* TypeAction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TypeAction.h; path = Actions/TypeAction.h; sourceTree = ""; }; 22998C7C197A85A300A4C691 /* TypeAction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TypeAction.m; path = Actions/TypeAction.m; sourceTree = ""; }; @@ -133,8 +137,11 @@ 223F8ECA15BDF8290078A313 /* ActionExecutor.m */, 32A70AAB03705E1F00C91783 /* cliclick_Prefix.pch */, 08FB7796FE84155DC02AAC07 /* cliclick.m */, + 22760C9420526A5000467D47 /* ExecutionOptions.h */, 2250A9281966D39000150E07 /* KeycodeInformer.h */, 2250A9291966D39000150E07 /* KeycodeInformer.m */, + 22760C912052632600467D47 /* OutputHandler.h */, + 22760C922052632600467D47 /* OutputHandler.m */, ); name = Source; sourceTree = ""; @@ -263,7 +270,7 @@ 08FB7793FE84155DC02AAC07 /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0830; + LastUpgradeCheck = 0920; }; buildConfigurationList = 1DEB927808733DD40010E9CD /* Build configuration list for PBXProject "cliclick" */; compatibilityVersion = "Xcode 3.2"; @@ -354,6 +361,7 @@ 22E5626F16BA589200CD5D86 /* KeyUpAction.m in Sources */, 22C68A501825A8960083A722 /* KeyDownUpBaseAction.m in Sources */, 22E5627016BA589200CD5D86 /* MouseBaseAction.m in Sources */, + 22760C932052632600467D47 /* OutputHandler.m in Sources */, 2250A92A1966D39000150E07 /* KeycodeInformer.m in Sources */, 22E5627116BA589200CD5D86 /* MoveAction.m in Sources */, 22E5627216BA589200CD5D86 /* PrintAction.m in Sources */, @@ -408,12 +416,18 @@ buildSettings = { ARCHS_STANDARD_32_64_BIT_PRE_XCODE_3_1 = "x86_64 i386 ppc"; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -441,12 +455,18 @@ buildSettings = { ARCHS_STANDARD_32_64_BIT_PRE_XCODE_3_1 = "x86_64 i386 ppc"; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; diff --git a/cliclick_Prefix.pch b/cliclick_Prefix.pch index 6625e38..26afb3a 100644 --- a/cliclick_Prefix.pch +++ b/cliclick_Prefix.pch @@ -7,33 +7,29 @@ #import #endif -// Compatibiliy macros for 10.6, which knows neither @123 nor @{@"Foo": @"Bar"} -#define N(x) [NSNumber numberWithInt: x] -#define D(...) [NSDictionary dictionaryWithObjectsAndKeys:__VA_ARGS__, nil] - // Version or branch -#define VERSION @"3.3" +#define VERSION @"4.0" // Date as m/d/Y -#define RELEASEDATE @"11/12/2016" +#define RELEASEDATE @"2018-03-24" #define MODE_REGULAR 0 #define MODE_VERBOSE 1 -#define MODE_TEST 2 +#define MODE_TEST 2 -#define MODIFIER_SHIFT 32 -#define MODIFIER_ALT 64 -#define MODIFIER_SHIFT_ALT (MODIFIER_SHIFT | MODIFIER_ALT) -#define NSNUMBER_MODIFIER_SHIFT [NSNumber numberWithInt:MODIFIER_SHIFT] -#define NSNUMBER_MODIFIER_ALT [NSNumber numberWithInt:MODIFIER_ALT] +#define MODIFIER_SHIFT 32 +#define MODIFIER_ALT 64 +#define MODIFIER_SHIFT_ALT (MODIFIER_SHIFT | MODIFIER_ALT) +#define NSNUMBER_MODIFIER_SHIFT [NSNumber numberWithInt:MODIFIER_SHIFT] +#define NSNUMBER_MODIFIER_ALT [NSNumber numberWithInt:MODIFIER_ALT] #define NSNUMBER_MODIFIER_SHIFT_ALT [NSNumber numberWithInt:MODIFIER_SHIFT_ALT] #define KEYCODE_SHIFT 56 -#define KEYCODE_ALT 58 +#define KEYCODE_ALT 58 #define CHARINFO_URL_TEMPLATE @"https://github.com/BlueM/cliclick/blob/%@/README-Characters.md" -#define HISTORY_URL @"http://www.bluem.net/jump/cliclick-versions/" -#define DONATIONS_URL @"http://www.bluem.net/jump/donations/" +#define HISTORY_URL @"https://www.bluem.net/jump/cliclick-versions/" +#define DONATIONS_URL @"https://www.bluem.net/jump/donations/" #ifdef DEBUG #define DLog(...) NSLog(@"%s %@", __PRETTY_FUNCTION__, [NSString stringWithFormat:__VA_ARGS__])