AXTyper 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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