AXTyper 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/.yardopts.typer ADDED
@@ -0,0 +1,10 @@
1
+ --no-cache
2
+ --no-output
3
+ --verbose
4
+ --markup markdown
5
+ --markup-provider redcarpet
6
+ --readme README.markdown.typer
7
+ lib/**/*.rb
8
+ ext/**/*.m
9
+ -
10
+ docs/KeyboardEvents.markdown
@@ -0,0 +1,74 @@
1
+ # AXTyper
2
+
3
+ This gem is a component of AXElements. It provides an interface for
4
+ posting keyboard events to the system as well as a mixin for parsing
5
+ a string into a series of events.
6
+
7
+ ## Demo
8
+
9
+ The basics:
10
+
11
+ ```ruby
12
+ require 'accessibility/string'
13
+
14
+ include Accessibility::String
15
+
16
+ keyboard_events_for("Hey, there!").each do |event|
17
+ KeyCoder.post_event event
18
+ end
19
+ ```
20
+
21
+ Something a bit more advanced:
22
+
23
+ ```ruby
24
+ require 'accessibility/string'
25
+
26
+ include Accessibility::String
27
+
28
+ keyboard_events_for("\\COMMAND+\t").each do |event|
29
+ KeyCoder.post_event event
30
+ end
31
+ ```
32
+
33
+ ## Testing
34
+
35
+ Running the AXElements test suite for only the AXTyper related tests
36
+ can be accomplished with the `test:string` task.
37
+
38
+ ```shell
39
+ rake test:string
40
+ ```
41
+
42
+ ## TODO
43
+
44
+ The API for posting events is ad-hoc for the sake of demonstration;
45
+ AXElements exposes this functionality via `Kernel#type`. The standalone
46
+ API provided here could be improved.
47
+
48
+ ## License
49
+
50
+ Copyright (c) 2012 Marketcircle Inc.
51
+ All rights reserved.
52
+
53
+ Redistribution and use in source and binary forms, with or without
54
+ modification, are permitted provided that the following conditions are met:
55
+ * Redistributions of source code must retain the above copyright
56
+ notice, this list of conditions and the following disclaimer.
57
+ * Redistributions in binary form must reproduce the above copyright
58
+ notice, this list of conditions and the following disclaimer in the
59
+ documentation and/or other materials provided with the distribution.
60
+ * Neither the name of Marketcircle Inc. nor the names of its
61
+ contributors may be used to endorse or promote products derived
62
+ from this software without specific prior written permission.
63
+
64
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
65
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
66
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
67
+ DISCLAIMED. IN NO EVENT SHALL Marketcircle Inc. BE LIABLE FOR ANY
68
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
69
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
70
+ GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
71
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
72
+ IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
73
+ OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
74
+ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,119 @@
1
+ # Keyboard Events
2
+
3
+ Keyboard events are a system provided by Apple that allows you to
4
+ simulate keyboard input. The API for this in the `ApplicationServices`
5
+ framework, but there is an analogue in the `Acessibility` APIs which
6
+ has the additional option of directing the input to a specific application.
7
+
8
+ Using accessibility actions and setting attributes you can already
9
+ perform most of the interactions that would be possible with the
10
+ keyboard simulation. However, there are some things that you will need
11
+ to, or it will just make more sense to, simulate keyboard input. For
12
+ example, to make use of hot keys you would have to add extra actions
13
+ or attributes to a control in the application; that would be more
14
+ work, possibly prone to error, than simply simulating the hot key from
15
+ outside the application. In other situations you may specifically want
16
+ to test out keyboard navigation and so actions would not be a good
17
+ substitute. It may be that the APIs that AXElements provides for
18
+ typing just make more sense when writing tests or scripts.
19
+
20
+ ## Typing with the DSL
21
+
22
+ The {Accessibility::DSL} mixin exposes keyboard events through the
23
+ `type` method. A simple example would look like this:
24
+
25
+ type "Hello, #{ENV['USER']}! How are you today?\n"
26
+
27
+ And watch your computer come to life! The `type` command takes an
28
+ additional optional parameter that we'll get to later. The first
29
+ parameter is just a string that you want AXElements to type out. How
30
+ to format the string should be obvious for the most part, but some
31
+ things like the command key and arrows might not be so obvious.
32
+
33
+ ## Formatting Strings
34
+
35
+ Letters and numbers should be written just as you would for any other
36
+ string. Any of the standard symbols can also be plainly added to a
37
+ string that you want to have typed. Here are some examples:
38
+
39
+ type "UPPER CASE LETTERS"
40
+ type "lower case letters"
41
+ type "1337 message @/\/|) 57|_||=|="
42
+ type "A proper sentence can be typed out (all at once)."
43
+
44
+ ### Regular Escape Sequences
45
+
46
+ Things like newlines and tabs should be formatted just like they would
47
+ in a regular string. That is, normal string escape sequences should
48
+ "just work" with AXElements. Here are some more examples:
49
+
50
+ type "Have a bad \b\b\b\b\b good day!"
51
+ type "First line.\nSecond line."
52
+ type "I \t like \t to \t use \t tabs \t a \t lot."
53
+ type "Explicit\sSpaces."
54
+
55
+ ### Custom Escape Sequences
56
+
57
+ Unfortunately, there is no built in escape sequence for deleting to
58
+ the right or pressing command keys like `F1`. AXElements defines some
59
+ extra escape sequences in order to easily represent the remaining
60
+ keys.
61
+
62
+ These custom escape sequences __shoud start with two `\` characters__,
63
+ as in this example:
64
+
65
+ type "\\F1"
66
+
67
+ A custom escape sequence __should terminate with a space or the end of
68
+ the string__, as in this example:
69
+
70
+ type "\\PAGEDOWN notice the space afterwards\\PAGEUP but not before"
71
+
72
+ The full list of supported custom escape sequences is listed in
73
+ {Accessibility::StringParser::ESCAPES}. Some escapes have an alias,
74
+ such as the right arrow key which can be escaped as `"\\RIGHT"` or as
75
+ `"\\->"`.
76
+
77
+ ### Hot Keys
78
+
79
+ To support pressing multiple keys at the same time, also known as hot
80
+ keys, you must start with the custom escape sequence for the
81
+ combination and instead of ending with a space you should put a `+`
82
+ character to chain the next key. The entire sequence should be ended
83
+ with a space or nil. Some common examples are opening a file or
84
+ quitting an application:
85
+
86
+ type "\\COMMAND+o"
87
+ type "\\CONTROL+a Typing at the start of the line"
88
+ type "\\COMMAND+\\SHIFT+s"
89
+
90
+ You might also note that `CMD+SHIFT+s` could also be:
91
+
92
+ type "\\COMMAND+S"
93
+
94
+ Since a capital `S` will cause the shift key to be held down.
95
+
96
+ ## Protips
97
+
98
+ In order make sure that certain sequences of characters are properly
99
+ escaped, it is recommended to simply always use double quoted
100
+ strings.
101
+
102
+ ### Posting To A Specific Application
103
+
104
+ The second argument to the `type` command can be an {AX::Application}
105
+ object. If you do not include the argument, the events will be posted
106
+ to the system, which usually means the application that currently is
107
+ active. Note that you cannot be more specific than the application
108
+ that you want to send the events to, within the application, the
109
+ control that has keyboard focus will receive the events.
110
+
111
+ ### Changing Typing Speed
112
+
113
+ You can set the typing speed at load time by setting the environment
114
+ variable `KEY_RATE`. See {Accessibility::Core::KEY\_RATE} for details on
115
+ possible values. An example of using it would be:
116
+
117
+ KEY_RATE=SLOW irb -rubygems -rax_elements
118
+ KEY_RATE=0.25 rspec gui_spec.rb
119
+
@@ -0,0 +1,12 @@
1
+ require 'mkmf'
2
+
3
+ $CFLAGS << ' -std=c99 -Wall -Werror -ObjC'
4
+ $LIBS << ' -framework Cocoa -framework Carbon -framework ApplicationServices'
5
+
6
+ if RUBY_ENGINE == 'macruby'
7
+ $CFLAGS << ' -fobjc-gc'
8
+ else
9
+ $CFLAGS << ' -DNOT_MACRUBY -fblocks'
10
+ end
11
+
12
+ create_makefile('accessibility/key_coder')
@@ -0,0 +1,87 @@
1
+ /*
2
+ * key_coder.c
3
+ * KeyCoder
4
+ *
5
+ * Created by Mark Rada on 11-07-27.
6
+ * Copyright 2011 Marketcircle Incorporated. All rights reserved.
7
+ */
8
+
9
+
10
+ #import <Cocoa/Cocoa.h>
11
+ #import <Carbon/Carbon.h>
12
+ #import <ApplicationServices/ApplicationServices.h>
13
+ #include "ruby.h"
14
+
15
+ static VALUE
16
+ rb_keycoder_dynamic_mapping()
17
+ {
18
+
19
+ VALUE map = rb_hash_new();
20
+
21
+ #ifdef NOT_MACRUBY
22
+ @autoreleasepool {
23
+ #endif
24
+
25
+ TISInputSourceRef keyboard = TISCopyCurrentKeyboardLayoutInputSource();
26
+ CFDataRef layout_data = (CFDataRef)TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData);
27
+ const UCKeyboardLayout* layout = (const UCKeyboardLayout*)CFDataGetBytePtr(layout_data);
28
+
29
+ void (^key_coder)(int) = ^(int key_code) {
30
+ UniChar string[255];
31
+ UniCharCount string_length = 0;
32
+ UInt32 dead_key_state = 0;
33
+ UCKeyTranslate(
34
+ layout,
35
+ key_code,
36
+ kUCKeyActionDown,
37
+ 0,
38
+ LMGetKbdType(), // kb type
39
+ 0, // OptionBits keyTranslateOptions,
40
+ &dead_key_state,
41
+ 255,
42
+ &string_length,
43
+ string
44
+ );
45
+
46
+ NSString* nsstring = [NSString stringWithCharacters:string length:string_length];
47
+ rb_hash_aset(map, rb_str_new_cstr([nsstring UTF8String]), INT2FIX(key_code));
48
+ };
49
+
50
+ // skip 65-92 since they are hard coded and do not change
51
+ for (int key_code = 0; key_code < 65; key_code++)
52
+ key_coder(key_code);
53
+ for (int key_code = 93; key_code < 127; key_code++)
54
+ key_coder(key_code);
55
+
56
+ #ifdef NOT_MACRUBY
57
+ CFRelease(keyboard);
58
+ }; // Close the autorelease pool
59
+ #else
60
+ CFMakeCollectable(keyboard);
61
+ #endif
62
+
63
+ return map;
64
+ }
65
+
66
+
67
+ static VALUE
68
+ rb_keycoder_post_event(VALUE self, VALUE event)
69
+ {
70
+ VALUE code = rb_ary_entry(event, 0);
71
+ VALUE state = rb_ary_entry(event, 1);
72
+
73
+ CGEventRef event_ref = CGEventCreateKeyboardEvent(NULL, FIX2LONG(code), state);
74
+ CGEventPost(kCGHIDEventTap, event_ref);
75
+
76
+ usleep(9000);
77
+ return Qtrue;
78
+ }
79
+
80
+
81
+ void
82
+ Init_key_coder()
83
+ {
84
+ VALUE rb_cKeyCoder = rb_define_class("KeyCoder", rb_cObject);
85
+ rb_define_singleton_method(rb_cKeyCoder, "dynamic_mapping", rb_keycoder_dynamic_mapping, 0);
86
+ rb_define_singleton_method(rb_cKeyCoder, "post_event", rb_keycoder_post_event, 1);
87
+ }
@@ -0,0 +1,488 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'accessibility/version'
4
+ require 'accessibility/key_coder'
5
+ framework 'ApplicationServices' if defined? MACRUBY_VERSION
6
+
7
+ ##
8
+ # Parses strings of human readable text into a series of events meant to
9
+ # be processed by {Accessibility::Core#post:to:} or {KeyCoder.post_event}.
10
+ #
11
+ # Supports most, if not all, latin keyboard layouts, maybe some
12
+ # international layouts as well.
13
+ module Accessibility::String
14
+
15
+ ##
16
+ # Generate keyboard events for the given string. Strings should be in a
17
+ # human readable with a few exceptions. Command key (e.g. control, option,
18
+ # command) should be written in string as they appear in
19
+ # {Accessibility::String::EventGenerator::CUSTOM}.
20
+ #
21
+ # For more details on event generation, read the
22
+ # {file:docs/KeyboardEvents.markdown Keyboard Events} documentation.
23
+ #
24
+ # @param [String]
25
+ # @return [Array<Array(Fixnum,Boolean)>]
26
+ def keyboard_events_for string
27
+ EventGenerator.new(Lexer.new(string).lex).generate
28
+ end
29
+
30
+ ##
31
+ # Tokenizer for strings. This class will take a string and break
32
+ # it up into chunks for the event generator. The structure generated
33
+ # here is an array that contains strings and recursively other arrays
34
+ # of strings and arrays of strings.
35
+ #
36
+ # @example
37
+ #
38
+ # Lexer.new("Hai").lex # => ['H','a','i']
39
+ # Lexer.new("\\CAPSLOCK").lex # => [["\\CAPSLOCK"]]
40
+ # Lexer.new("\\COMMAND+a").lex # => [["\\COMMAND", ['a']]]
41
+ # Lexer.new("One\nTwo").lex # => ['O','n','e',"\n",'T','w','o']
42
+ #
43
+ class Lexer
44
+
45
+ ##
46
+ # Once a string is lexed, this contains the tokenized structure.
47
+ #
48
+ # @return [Array<String,Array<String,...>]
49
+ attr_accessor :tokens
50
+
51
+ # @param [#to_s]
52
+ def initialize string
53
+ @chars = string.to_s
54
+ @tokens = []
55
+ end
56
+
57
+ ##
58
+ # Tokenize the string that the lexer was initialized with and
59
+ # return the sequence of tokens that were lexed.
60
+ #
61
+ # @return [Array<String,Array<String,...>]
62
+ def lex
63
+ length = @chars.length
64
+ @index = 0
65
+ while @index < length
66
+ @tokens << if custom?
67
+ lex_custom
68
+ else
69
+ lex_char
70
+ end
71
+ @index += 1
72
+ end
73
+ @tokens
74
+ end
75
+
76
+
77
+ private
78
+
79
+ ##
80
+ # Is it a real custom escape? Kind of a lie, there is one
81
+ # case it does not handle--they get handled in the generator,
82
+ # but maybe they should be handled here?
83
+ # - An upper case letter or symbol following `"\\"` that is
84
+ # not mapped
85
+ def custom?
86
+ @chars[@index] == CUSTOM_ESCAPE &&
87
+ (next_char = @chars[@index+1]) &&
88
+ next_char == next_char.upcase &&
89
+ next_char != SPACE
90
+ end
91
+
92
+ # @return [Array]
93
+ def lex_custom
94
+ start = @index
95
+ loop do
96
+ char = @chars[@index]
97
+ if char == PLUS
98
+ if @chars[@index-1] == CUSTOM_ESCAPE
99
+ @index += 1
100
+ return custom_subseq start
101
+ else
102
+ tokens = custom_subseq start
103
+ @index += 1
104
+ return tokens << lex_custom
105
+ end
106
+ elsif char == SPACE
107
+ return custom_subseq start
108
+ elsif char == nil
109
+ raise ArgumentError, "Bad escape sequence" if start == @index
110
+ return custom_subseq start
111
+ else
112
+ @index += 1
113
+ end
114
+ end
115
+ end
116
+
117
+ # @return [Array]
118
+ def custom_subseq start
119
+ [@chars[start...@index]]
120
+ end
121
+
122
+ # @return [String]
123
+ def lex_char
124
+ @chars[@index]
125
+ end
126
+
127
+ # @private
128
+ SPACE = " "
129
+ # @private
130
+ PLUS = "+"
131
+ # @private
132
+ CUSTOM_ESCAPE = "\\"
133
+ end
134
+
135
+
136
+ ##
137
+ # @todo Add a method to generate just keydown or just keyup events.
138
+ # Requires separating code lookup from event creation.
139
+ #
140
+ # Generate a sequence of keyboard events given a sequence of tokens.
141
+ # The token format is defined by the {Lexer} class output; it is best
142
+ # to use that class to generate the tokens.
143
+ #
144
+ # @example
145
+ #
146
+ # # Upper case 'A'
147
+ # EventGenerator.new(["A"]).generate # => [[56,true],[70,true],[70,false],[56,false]]
148
+ #
149
+ # # Press the caps lock button, turn it on
150
+ # EventGenerator.new([["\\CAPS"]]).generate # => [[0x39,true],[0x39,false]]
151
+ #
152
+ # # Hotkey, press and hold command key and then 'a', then release both
153
+ # EventGenerator.new([["\\CMD",["a"]]]).generate # => [[55,true],[70,true],[70,false],[55,false]]
154
+ #
155
+ # # Press the return/enter key
156
+ # EventGenerator.new(["\n"]).generate # => [[10,true],[10,false]]
157
+ #
158
+ class EventGenerator
159
+
160
+ ##
161
+ # Regenerate the portion of the key mapping that is set dynamically
162
+ # based on keyboard layout (e.g. US, Dvorak, etc.).
163
+ #
164
+ # This method should be called whenever the keyboard layout changes.
165
+ # This can be called automatically by registering for a notification
166
+ # in a run looped environment.
167
+ def self.regenerate_dynamic_mapping
168
+ # KeyCoder is declared in the Objective-C extension
169
+ MAPPING.merge! KeyCoder.dynamic_mapping
170
+ # Also add an alias to the mapping
171
+ MAPPING["\n"] = MAPPING["\r"]
172
+ end
173
+
174
+ ##
175
+ # Dynamic mapping of characters to keycodes. The map is generated at
176
+ # startup time in order to support multiple keyboard layouts.
177
+ #
178
+ # @return [Hash{String=>Fixnum}]
179
+ MAPPING = {}
180
+
181
+ # Initialize the table
182
+ regenerate_dynamic_mapping
183
+
184
+ ##
185
+ # @note These mappings are all static and come from `/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h`
186
+ #
187
+ # Map of custom escape sequences to their hardcoded keycode value.
188
+ #
189
+ # @return [Hash{String=>Fixnum}]
190
+ CUSTOM = {
191
+ "\\ESCAPE" => 0x35,
192
+ "\\ESC" => 0x35,
193
+ "\\COMMAND" => 0x37,
194
+ "\\CMD" => 0x37,
195
+ "\\SHIFT" => 0x38,
196
+ "\\LSHIFT" => 0x38,
197
+ "\\CAPS" => 0x39,
198
+ "\\CAPSLOCK" => 0x39,
199
+ "\\OPTION" => 0x3A,
200
+ "\\OPT" => 0x3A,
201
+ "\\ALT" => 0x3A,
202
+ "\\CONTROL" => 0x3B,
203
+ "\\CTRL" => 0x3B,
204
+ "\\RSHIFT" => 0x3C,
205
+ "\\ROPTION" => 0x3D,
206
+ "\\ROPT" => 0x3D,
207
+ "\\RALT" => 0x3D,
208
+ "\\RCONTROL" => 0x3E,
209
+ "\\RCTRL" => 0x3E,
210
+ "\\FUNCTION" => 0x3F,
211
+ "\\FN" => 0x3F,
212
+ "\\VOLUMEUP" => 0x48,
213
+ "\\VOLUP" => 0x48,
214
+ "\\VOLUMEDOWN" => 0x49,
215
+ "\\VOLDOWN" => 0x49,
216
+ "\\MUTE" => 0x4A,
217
+ "\\F1" => 0x7A,
218
+ "\\F2" => 0x78,
219
+ "\\F3" => 0x63,
220
+ "\\F4" => 0x76,
221
+ "\\F5" => 0x60,
222
+ "\\F6" => 0x61,
223
+ "\\F7" => 0x62,
224
+ "\\F8" => 0x64,
225
+ "\\F9" => 0x65,
226
+ "\\F10" => 0x6D,
227
+ "\\F11" => 0x67,
228
+ "\\F12" => 0x6F,
229
+ "\\F13" => 0x69,
230
+ "\\F14" => 0x6B,
231
+ "\\F15" => 0x71,
232
+ "\\F16" => 0x6A,
233
+ "\\F17" => 0x40,
234
+ "\\F18" => 0x4F,
235
+ "\\F19" => 0x50,
236
+ "\\F20" => 0x5A,
237
+ "\\HELP" => 0x72,
238
+ "\\HOME" => 0x73,
239
+ "\\END" => 0x77,
240
+ "\\PAGEUP" => 0x74,
241
+ "\\PAGEDOWN" => 0x79,
242
+ "\\DELETE" => 0x75,
243
+ "\\LEFT" => 0x7B,
244
+ "\\<-" => 0x7B,
245
+ "\\RIGHT" => 0x7C,
246
+ "\\->" => 0x7C,
247
+ "\\DOWN" => 0x7D,
248
+ "\\UP" => 0x7E,
249
+ "\\0" => 0x52,
250
+ "\\1" => 0x53,
251
+ "\\2" => 0x54,
252
+ "\\3" => 0x55,
253
+ "\\4" => 0x56,
254
+ "\\5" => 0x57,
255
+ "\\6" => 0x58,
256
+ "\\7" => 0x59,
257
+ "\\8" => 0x5B,
258
+ "\\9" => 0x5C,
259
+ "\\Decimal" => 0x41,
260
+ "\\." => 0x41,
261
+ "\\Plus" => 0x45,
262
+ "\\+" => 0x45,
263
+ "\\Multiply" => 0x43,
264
+ "\\*" => 0x43,
265
+ "\\Minus" => 0x4E,
266
+ "\\-" => 0x4E,
267
+ "\\Divide" => 0x4B,
268
+ "\\/" => 0x4B,
269
+ "\\Equals" => 0x51,
270
+ "\\=" => 0x51,
271
+ "\\Enter" => 0x4C,
272
+ "\\Clear" => 0x47,
273
+ }
274
+
275
+ ##
276
+ # Mapping of shifted (characters written when holding shift) characters
277
+ # to keycodes.
278
+ #
279
+ # @return [Hash{String=>Fixnum}]
280
+ SHIFTED = {
281
+ '~' => '`',
282
+ '!' => '1',
283
+ '@' => '2',
284
+ '#' => '3',
285
+ '$' => '4',
286
+ '%' => '5',
287
+ '^' => '6',
288
+ '&' => '7',
289
+ '*' => '8',
290
+ '(' => '9',
291
+ ')' => '0',
292
+ '{' => '[',
293
+ '}' => ']',
294
+ '?' => '/',
295
+ '+' => '=',
296
+ '|' => "\\",
297
+ ':' => ';',
298
+ '_' => '-',
299
+ '"' => "'",
300
+ '<' => ',',
301
+ '>' => '.',
302
+ 'A' => 'a',
303
+ 'B' => 'b',
304
+ 'C' => 'c',
305
+ 'D' => 'd',
306
+ 'E' => 'e',
307
+ 'F' => 'f',
308
+ 'G' => 'g',
309
+ 'H' => 'h',
310
+ 'I' => 'i',
311
+ 'J' => 'j',
312
+ 'K' => 'k',
313
+ 'L' => 'l',
314
+ 'M' => 'm',
315
+ 'N' => 'n',
316
+ 'O' => 'o',
317
+ 'P' => 'p',
318
+ 'Q' => 'q',
319
+ 'R' => 'r',
320
+ 'S' => 's',
321
+ 'T' => 't',
322
+ 'U' => 'u',
323
+ 'V' => 'v',
324
+ 'W' => 'w',
325
+ 'X' => 'x',
326
+ 'Y' => 'y',
327
+ 'Z' => 'z',
328
+ }
329
+
330
+ ##
331
+ # Mapping of optioned (characters written when holding option/alt)
332
+ # characters to keycodes.
333
+ #
334
+ # @return [Hash{String=>Fixnum}]
335
+ OPTIONED = {
336
+ '¡' => '1',
337
+ '™' => '2',
338
+ '£' => '3',
339
+ '¢' => '4',
340
+ '∞' => '5',
341
+ '§' => '6',
342
+ '¶' => '7',
343
+ '•' => '8',
344
+ 'ª' => '9',
345
+ 'º' => '0',
346
+ '“' => '[',
347
+ '‘' => ']',
348
+ 'æ' => "'",
349
+ '≤' => ',',
350
+ '≥' => '.',
351
+ 'π' => 'p',
352
+ '¥' => 'y',
353
+ 'ƒ' => 'f',
354
+ '©' => 'g',
355
+ '®' => 'r',
356
+ '¬' => 'l',
357
+ '÷' => '/',
358
+ '≠' => '=',
359
+ '«' => "\\",
360
+ 'å' => 'a',
361
+ 'ø' => 'o',
362
+ '´' => 'e',
363
+ '¨' => 'u',
364
+ 'ˆ' => 'i',
365
+ '∂' => 'd',
366
+ '˙' => 'h',
367
+ '†' => 't',
368
+ '˜' => 'n',
369
+ 'ß' => 's',
370
+ '–' => '-',
371
+ '…' => ';',
372
+ 'œ' => 'q',
373
+ '∆' => 'j',
374
+ '˚' => 'k',
375
+ '≈' => 'x',
376
+ '∫' => 'b',
377
+ 'µ' => 'm',
378
+ '∑' => 'w',
379
+ '√' => 'v',
380
+ 'Ω' => 'z',
381
+ }
382
+
383
+
384
+ ##
385
+ # Once {generate} is called, this contains the sequence of
386
+ # events.
387
+ #
388
+ # @return [Array<Array(Fixnum,Boolean)>]
389
+ attr_reader :events
390
+
391
+ # @param [Array<String,Array<String,Array...>>]
392
+ def initialize tokens
393
+ @tokens = tokens
394
+ # *3 since the output array will be at least *2 the
395
+ # number of tokens passed in, but will often be larger
396
+ # due to shifted/optioned characters and custom escapes;
397
+ # though a better number could be derived from
398
+ # analyzing common input...
399
+ @events = Array.new tokens.size*3
400
+ end
401
+
402
+ ##
403
+ # Generate the events for the tokens the event generator
404
+ # was initialized with. Returns the generated events.
405
+ #
406
+ # @return [Array<Array(Fixnum,Boolean)>]
407
+ def generate
408
+ @index = 0
409
+ gen_all @tokens
410
+ @events.compact!
411
+ @events
412
+ end
413
+
414
+
415
+ private
416
+
417
+ def add event
418
+ @events[@index] = event
419
+ @index += 1
420
+ end
421
+
422
+ def gen_all tokens
423
+ tokens.each do |token|
424
+ if token.kind_of? Array
425
+ gen_nested token.first, token[1..-1]
426
+ else
427
+ gen_single token
428
+ end
429
+ end
430
+ end
431
+
432
+ def gen_nested head, tail
433
+ if code = CUSTOM[head] || SHIFTED[head] || OPTIONED[head] || MAPPING[head]
434
+ add [code, true]
435
+ gen_all tail
436
+ add [code, false]
437
+ else # handling a special case
438
+ gen_all head.split(EMPTY_STRING)
439
+ end
440
+ end
441
+
442
+ def gen_single token
443
+ ((code = MAPPING[token]) && gen_dynamic(code)) ||
444
+ ((code = SHIFTED[token]) && gen_shifted(code)) ||
445
+ ((code = OPTIONED[token]) && gen_optioned(code)) ||
446
+ raise(ArgumentError, "#{token.inspect} has no mapping, bail!")
447
+ end
448
+
449
+ def gen_shifted code
450
+ add SHIFT_DOWN
451
+ gen_dynamic MAPPING[code]
452
+ add SHIFT_UP
453
+ end
454
+
455
+ def gen_optioned code
456
+ add OPTION_DOWN
457
+ gen_dynamic MAPPING[code]
458
+ add OPTION_UP
459
+ end
460
+
461
+ def gen_dynamic code
462
+ add [code, true]
463
+ add [code, false]
464
+ end
465
+
466
+ # @private
467
+ EMPTY_STRING = ''
468
+ # @private
469
+ OPTION_DOWN = [58, true]
470
+ # @private
471
+ OPTION_UP = [58, false]
472
+ # @private
473
+ SHIFT_DOWN = [56, true]
474
+ # @private
475
+ SHIFT_UP = [56, false]
476
+ end
477
+
478
+ end
479
+
480
+
481
+ ##
482
+ # @note This will only work if a run loop is running
483
+ #
484
+ # Register to be notified if the keyboard layout changes at runtime
485
+ # NSDistributedNotificationCenter.defaultCenter.addObserver Accessibility::String::EventGenerator,
486
+ # selector: 'regenerate_dynamic_mapping',
487
+ # name: KTISNotifySelectedKeyboardInputSourceChanged,
488
+ # object: nil
@@ -0,0 +1,7 @@
1
+ module Accessibility
2
+ # @return [String]
3
+ VERSION = '0.7.0'
4
+
5
+ # @return [String]
6
+ CODE_NAME = 'Clefairy'
7
+ end
data/test/runner.rb ADDED
@@ -0,0 +1,24 @@
1
+ require 'rubygems'
2
+ gem 'minitest'
3
+ require 'minitest/autorun'
4
+
5
+ # preprocessor powers, assemble!
6
+ if ENV['BENCH']
7
+ require 'minitest/benchmark'
8
+ else
9
+ require'minitest/pride'
10
+ end
11
+
12
+
13
+ class MiniTest::Unit::TestCase
14
+
15
+ # You may need this to help track down an issue if a test is crashing MacRuby
16
+ # def self.test_order
17
+ # :alpha
18
+ # end
19
+
20
+ def self.bench_range
21
+ bench_exp 100, 100_000
22
+ end
23
+
24
+ end
@@ -0,0 +1,233 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'accessibility/string'
4
+
5
+ class TestAccessibilityStringLexer < MiniTest::Unit::TestCase
6
+
7
+ def lex string
8
+ Accessibility::String::Lexer.new(string).lex
9
+ end
10
+
11
+ def test_lex_simple_string
12
+ assert_equal [], lex('')
13
+ assert_equal ['"',"J","u","s","t"," ","W","o","r","k","s",'"',"™"], lex('"Just Works"™')
14
+ assert_equal ["M","i","l","k",","," ","s","h","a","k","e","."], lex("Milk, shake.")
15
+ assert_equal ["D","B","7"], lex("DB7")
16
+ end
17
+
18
+ def test_lex_single_custom_escape
19
+ assert_equal [["\\CMD"]], lex("\\CMD")
20
+ assert_equal [["\\1"]], lex("\\1")
21
+ assert_equal [["\\F1"]], lex("\\F1")
22
+ assert_equal [["\\*"]], lex("\\*")
23
+ end
24
+
25
+ def test_lex_hotkey_custom_escape
26
+ assert_equal [["\\COMMAND",[","]]], lex("\\COMMAND+,")
27
+ assert_equal [["\\COMMAND",["\\SHIFT",["s"]]]], lex("\\COMMAND+\\SHIFT+s")
28
+ assert_equal [["\\COMMAND",["\\+"]]], lex("\\COMMAND+\\+")
29
+ assert_equal [["\\FN",["\\F10"]]], lex("\\FN+\\F10")
30
+ end
31
+
32
+ def test_lex_ruby_escapes
33
+ assert_equal ["\n","\r","\t","\b"], lex("\n\r\t\b")
34
+ assert_equal ["O","n","e","\n","T","w","o"], lex("One\nTwo")
35
+ assert_equal ["L","i","e","\b","\b","\b","d","e","l","i","s","h"], lex("Lie\b\b\bdelish")
36
+ end
37
+
38
+ def test_lex_complex_string
39
+ assert_equal ["T","e","s","t",["\\CMD",["s"]]], lex("Test\\CMD+s")
40
+ assert_equal ["Z","O","M","G"," ","1","3","3","7","!","!","1"], lex("ZOMG 1337!!1")
41
+ assert_equal ["F","u","u","!","@","#","%",["\\CMD",["a"]],"\b"], lex("Fuu!@#%\\CMD+a \b")
42
+ assert_equal [["\\CMD",["a"]],"\b","A","l","l"," ","g","o","n","e","!"], lex("\\CMD+a \bAll gone!")
43
+ end
44
+
45
+ def test_lex_backslash # make sure we handle these edge cases predictably
46
+ assert_equal ["\\"], lex("\\")
47
+ assert_equal ["\\"," "], lex("\\ ")
48
+ assert_equal ["\\","h","m","m"], lex("\\hmm")
49
+ assert_equal [["\\HMM"]], lex("\\HMM") # the one missed case
50
+ end
51
+
52
+ def test_lex_plus_escape
53
+ assert_equal [["\\+"]], lex("\\+")
54
+ end
55
+
56
+ def test_lex_bad_custom_escape_sequence
57
+ assert_raises ArgumentError do
58
+ lex("\\COMMAND+")
59
+ end
60
+ end
61
+
62
+ end
63
+
64
+
65
+ class TestAccessibilityStringEventGenerator < MiniTest::Unit::TestCase
66
+
67
+ def gen tokens
68
+ Accessibility::String::EventGenerator.new(tokens).generate
69
+ end
70
+
71
+ def map; @@map ||= KeyCoder.dynamic_mapping; end
72
+
73
+ def t; true; end
74
+ def f; false; end
75
+
76
+ def a; @@a ||= map['a']; end
77
+ def c; @@c ||= map['c']; end
78
+ def e; @@e ||= map['e']; end
79
+ def h; @@h ||= map['h']; end
80
+ def i; @@i ||= map['i']; end
81
+ def k; @@k ||= map['k']; end
82
+ def m; @@m ||= map["m"]; end
83
+
84
+ def two; @@two ||= map['2']; end
85
+ def four; @@four ||= map['4']; end
86
+
87
+ def retern; @@retern ||= map["\r"]; end
88
+ def tab; @@tab ||= map["\t"]; end
89
+ def space; @@space ||= map["\s"]; end
90
+
91
+ def dash; @@dash ||= map["-"]; end
92
+ def comma; @@comma ||= map[","]; end
93
+ def apos; @@apos ||= map["'"]; end
94
+ def at; @@at ||= map["2"]; end
95
+ def paren; @@paren ||= map["9"]; end
96
+ def chev; @@chev ||= map["."]; end
97
+
98
+ def sigma; @@sigma ||= map["w"]; end
99
+ def tm; @@tm ||= map["2"]; end
100
+ def gbp; @@gbp ||= map["3"]; end
101
+ def omega; @@omega ||= map["z"]; end
102
+
103
+ def bslash; @@blash ||= map["\\"]; end
104
+
105
+ # key code for the left shift key
106
+ def sd; [56,t]; end
107
+ def su; [56,f]; end
108
+
109
+ # key code for the left option key
110
+ def od; [58,t]; end
111
+ def ou; [58,f]; end
112
+
113
+ # key code for the left command key
114
+ def cd; [0x37,t]; end
115
+ def cu; [0x37,f]; end
116
+
117
+ # key code for right arrow key
118
+ def rd; [0x7c,t]; end
119
+ def ru; [0x7c,f]; end
120
+
121
+ # key code for left control key
122
+ def ctrld; [0x3B,t]; end
123
+ def ctrlu; [0x3B,f]; end
124
+
125
+
126
+ def test_generate_lowercase
127
+ assert_equal [[a,t],[a,f]], gen(['a'])
128
+ assert_equal [[c,t],[c,f],[k,t],[k,f]], gen(['c','k'])
129
+ assert_equal [[e,t],[e,f],[e,t],[e,f]], gen(['e','e'])
130
+ assert_equal [[c,t],[c,f],[a,t],[a,f],[k,t],[k,f],[e,t],[e,f]], gen(['c','a','k','e'])
131
+ end
132
+
133
+ def test_generate_uppercase
134
+ assert_equal [sd,[a,t],[a,f],su], gen(['A'])
135
+ assert_equal [sd,[c,t],[c,f],su,sd,[k,t],[k,f],su], gen(['C','K'])
136
+ assert_equal [sd,[e,t],[e,f],su,sd,[e,t],[e,f],su], gen(['E','E'])
137
+ assert_equal [sd,[c,t],[c,f],su,sd,[a,t],[a,f],su,sd,[k,t],[k,f],su], gen(['C','A','K'])
138
+ end
139
+
140
+ def test_generate_numbers
141
+ assert_equal [[two,t],[two,f]], gen(['2'])
142
+ assert_equal [[four,t],[four,f],[two,t],[two,f]], gen(['4','2'])
143
+ assert_equal [[two,t],[two,f],[two,t],[two,f]], gen(['2','2'])
144
+ end
145
+
146
+ def test_generate_ruby_escapes
147
+ assert_equal [[retern,t],[retern,f]], gen(["\r"])
148
+ assert_equal [[retern,t],[retern,f]], gen(["\n"])
149
+ assert_equal [[tab,t],[tab,f]], gen(["\t"])
150
+ assert_equal [[space,t],[space,f]], gen(["\s"])
151
+ assert_equal [[space,t],[space,f]], gen([" "])
152
+ end
153
+
154
+ def test_generate_symbols
155
+ assert_equal [[dash,t],[dash,f]], gen(["-"])
156
+ assert_equal [[comma,t],[comma,f]], gen([","])
157
+ assert_equal [[apos,t],[apos,f]], gen(["'"])
158
+ assert_equal [sd,[at,t],[at,f],su], gen(["@"])
159
+ assert_equal [sd,[paren,t],[paren,f],su], gen(["("])
160
+ assert_equal [sd,[chev,t],[chev,f],su], gen([">"])
161
+ end
162
+
163
+ def test_generate_unicode # holding option
164
+ assert_equal [od,[sigma,t],[sigma,f],ou], gen(["∑"])
165
+ assert_equal [od,[tm,t],[tm,f],ou], gen(["™"])
166
+ assert_equal [od,[gbp,t],[gbp,f],ou], gen(["£"])
167
+ assert_equal [od,[omega,t],[omega,f],ou], gen(["Ω"])
168
+ end
169
+
170
+ def test_generate_backslashes
171
+ assert_equal [[bslash,t],[bslash,f]], gen(["\\"])
172
+ assert_equal [[bslash,t],[bslash,f],[space,t],[space,f]], gen(["\\"," "])
173
+ assert_equal [[bslash,t],[bslash,f],[h,t],[h,f],[m,t],[m,f]], gen(["\\",'h','m'])
174
+ # is this the job of the parser or the lexer?
175
+ assert_equal [[bslash,t],[bslash,f],sd,[h,t],[h,f],su,sd,[m,t],[m,f],su], gen([["\\HM"]])
176
+ end
177
+
178
+ def test_generate_a_custom_escape
179
+ assert_equal [cd,cu], gen([["\\COMMAND"]])
180
+ assert_equal [cd,cu], gen([["\\CMD"]])
181
+ assert_equal [ctrld,ctrlu], gen([["\\CONTROL"]])
182
+ assert_equal [ctrld,ctrlu], gen([["\\CTRL"]])
183
+ end
184
+
185
+ def test_generate_hotkey
186
+ assert_equal [ctrld,[a,t],[a,f],ctrlu], gen([["\\CONTROL",["a"]]])
187
+ assert_equal [cd,sd,rd,ru,su,cu], gen([["\\COMMAND",['\SHIFT',['\->']]]])
188
+ end
189
+
190
+ def test_generate_real_use # a regression
191
+ assert_equal [ctrld,[a,t],[a,f],ctrlu,[h,t],[h,f]], gen([["\\CTRL",["a"]],"h"])
192
+ end
193
+
194
+ def test_bails_for_unmapped_token
195
+ e = assert_raises ArgumentError do
196
+ gen(["☃"]) # cannot generate snowmen :(
197
+ end
198
+ assert_match /bail/i, e.message
199
+ end
200
+
201
+ def test_generate_arbitrary_nested_array_sequence
202
+ assert_equal [[c,t],[a,t],[k,t],[e,t],[e,f],[k,f],[a,f],[c,f]], gen([["c",["a",["k",["e"]]]]])
203
+ end
204
+
205
+ end
206
+
207
+
208
+ # NOTE: DO NOT TEST POSTING EVENTS HERE
209
+ # We only want to test posting events if all the tests in this file pass,
210
+ # otherwise the posted events may be unpredictable depending on what fails.
211
+ # Test event posting in the integration tests.
212
+ class TestAccessibilityString < MiniTest::Unit::TestCase
213
+ include Accessibility::String
214
+
215
+ # basic test to make sure the lexer and generator get along
216
+ def test_keyboard_events_for
217
+ events = keyboard_events_for 'cheezburger'
218
+ assert_kind_of Array, events
219
+ refute_empty events
220
+
221
+ assert_equal true, events[0][1]
222
+ assert_equal false, events[1][1]
223
+ end
224
+
225
+ def test_dynamic_map_initialized
226
+ refute_empty Accessibility::String::EventGenerator::MAPPING
227
+ end
228
+
229
+ def test_can_parse_empty_string
230
+ assert_equal [], keyboard_events_for('')
231
+ end
232
+
233
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: AXTyper
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.7.0
6
+ platform: ruby
7
+ authors:
8
+ - Mark Rada
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-03-29 00:00:00 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: minitest
16
+ prerelease: false
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ~>
21
+ - !ruby/object:Gem::Version
22
+ version: "2.11"
23
+ type: :development
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: "2.11"
30
+ - !ruby/object:Gem::Dependency
31
+ name: yard
32
+ prerelease: false
33
+ requirement: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - ~>
37
+ - !ruby/object:Gem::Version
38
+ version: 0.7.5
39
+ type: :development
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 0.7.5
46
+ - !ruby/object:Gem::Dependency
47
+ name: redcarpet
48
+ prerelease: false
49
+ requirement: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: "1.17"
55
+ type: :development
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: "1.17"
62
+ description: 'Simulate keyboard input via the Mac OS X Accessibility Framework. This
63
+
64
+ gem is a component of AXElements.
65
+
66
+ '
67
+ email: mrada@marketcircle.com
68
+ executables: []
69
+ extensions:
70
+ - ext/accessibility/key_coder/extconf.rb
71
+ extra_rdoc_files:
72
+ - README.markdown.typer
73
+ - .yardopts.typer
74
+ - docs/KeyboardEvents.markdown
75
+ files:
76
+ - lib/accessibility/version.rb
77
+ - lib/accessibility/string.rb
78
+ - ext/accessibility/key_coder/key_coder.c
79
+ - ext/accessibility/key_coder/extconf.rb
80
+ - test/unit/accessibility/test_string.rb
81
+ - test/runner.rb
82
+ - README.markdown.typer
83
+ - .yardopts.typer
84
+ - docs/KeyboardEvents.markdown
85
+ homepage: http://github.com/Marketcircle/AXElements
86
+ licenses:
87
+ - BSD 3-clause
88
+ post_install_message:
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ none: false
94
+ requirements:
95
+ - - '>='
96
+ - !ruby/object:Gem::Version
97
+ version: "0"
98
+ required_rubygems_version: !ruby/object:Gem::Requirement
99
+ none: false
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: "0"
104
+ requirements: []
105
+ rubyforge_project:
106
+ rubygems_version: 1.8.20
107
+ signing_key:
108
+ specification_version: 3
109
+ summary: Keyboard simulation via accessibility
110
+ test_files:
111
+ - test/unit/accessibility/test_string.rb
112
+ - test/runner.rb