accessibility_keyboard 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,14 @@
1
+ --no-cache
2
+ --no-output
3
+ --verbose
4
+ --markup markdown
5
+ --markup-provider redcarpet
6
+ --asset docs/images:images
7
+ --readme README.markdown
8
+ --hide-void-return
9
+ lib/**/*.rb
10
+ ext/**/*{.m,.c}
11
+ -
12
+ History.markdown
13
+ CONTRIBUTING.markdown
14
+
@@ -0,0 +1,44 @@
1
+ # Contributing to accessibility\_keyboard
2
+
3
+ First, let me thank you for wanting to contribute! :)
4
+
5
+ There are no contributions that are too small. Even if you are only fixing
6
+ typos, adding documentation, or some example code. The goal is to make the
7
+ process simple and as painless as possible to encourage contributions.
8
+
9
+
10
+ ## Reporting Bugs
11
+
12
+ If you think you've found a bug, feel free to log the issue. It is OK to
13
+ make a duplicate bug report; such reports will be closed as duplicates
14
+ with a pointer to the original issue.
15
+
16
+ You may also wish to see if the bug you've reported has already been
17
+ fixed or obsoleted on the master branch.
18
+
19
+
20
+ ## Code
21
+
22
+ Try to stick within the existing style. The importance of sticking with
23
+ existing style grows linearly with the size of the contribution.
24
+
25
+ It is preferred that code contributions, whether they are new features or
26
+ bug fixes, be accompanied by test(s) where appropriate.
27
+
28
+ The best way to submit code is through a
29
+ [pull request](https://help.github.com/articles/using-pull-requests) on the
30
+ [Github page](https://github.com/AXElements/accessibility_keyboard),
31
+ though email is also acceptable if privacy is a concern. Unless otherwise
32
+ requested, commit history will always be made to reflect who made the
33
+ contribution.
34
+
35
+
36
+ ## Documentation and Examples
37
+
38
+ As with code, try to fit with the existing style. New examples are always
39
+ welcome and the wiki could always use a new article. :)
40
+
41
+ The documentation in the wiki is open and no permission is required to make
42
+ edits or contributions. For API documentation or example code, it is best to
43
+ submit a pull request (or create a new wiki page if appropriate).
44
+
@@ -0,0 +1,4 @@
1
+ # 1.0.0
2
+
3
+ * Initial release
4
+
@@ -0,0 +1,98 @@
1
+ # keyboard
2
+
3
+ This gem is a component of
4
+ [AXElements](http://github.com/Marketcircle/AXElements). It provides
5
+ an interface for posting keyboard events to the system as well as a
6
+ mixin for parsing a string into a series of events.
7
+
8
+
9
+ A port of keyboard simulator code from
10
+ [AXElements](http://github.com/AXElements/AXElements),
11
+ but cleaned up and released as its own gem.
12
+
13
+ By itself, the `accessibility_keyboard` gem has limited use; but in
14
+ combination with a gem for performing other GUI manipulations, like
15
+ AXElements, this gem is very powerful and can be used for tasks such
16
+ as automated functional testing.
17
+
18
+ [![Dependency Status](https://gemnasium.com/AXElements/accessibility_keyboard.png)](https://gemnasium.com/AXElements/accessibility_keyboard)
19
+ [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/AXElements/accessibility_keyboard)
20
+ [![Build Status](https://travis-ci.org/AXElements/accessibility_keyboard.png?branch=master)](https://travis-ci.org/AXElements/accessibility_keyboard)
21
+
22
+
23
+ ## Examples
24
+
25
+ The basics:
26
+
27
+ ```ruby
28
+ require 'accessibility/keyboard'
29
+
30
+ include Accessibility::Keyboard
31
+
32
+ keyboard_events_for("Hey, there!").each do |event|
33
+ KeyCoder.post_event event
34
+ end
35
+ ```
36
+
37
+ Something a bit more advanced:
38
+
39
+ ```ruby
40
+ require 'accessibility/keyboard'
41
+
42
+ include Accessibility::Keyboard
43
+
44
+ keyboard_events_for("\\COMMAND+\t").each do |event|
45
+ KeyCoder.post_event event
46
+ end
47
+ ```
48
+
49
+
50
+ ## Documentation
51
+
52
+ - [Keyboarding Blog Post](http://ferrous26.com/blog/2012/04/03/axelements-part1/).
53
+ - [API documentation](http://rdoc.info/gems/accessibility_keyboard/frames)
54
+ - The AXElements [keyboarding tutorial](https://github.com/AXElements/AXElements/wiki/Keyboarding)
55
+
56
+
57
+ ## Development
58
+
59
+ Development of this library happens as part of AXElements, but tests
60
+ and the API for this component remain separate so that it can be
61
+ released as part of the `accessibility_keyboard` gem.
62
+
63
+
64
+ ### TODO
65
+
66
+ The API for posting events is ad-hoc for the sake of demonstration;
67
+ AXElements exposes this functionality via `Kernel#type`. The standalone
68
+ API provided here could be improved.
69
+
70
+
71
+ ## Copyright
72
+
73
+ Copyright (c) 2013, Mark Rada
74
+ All rights reserved.
75
+
76
+ Redistribution and use in source and binary forms, with or without
77
+ modification, are permitted provided that the following conditions are met:
78
+
79
+ * Redistributions of source code must retain the above copyright
80
+ notice, this list of conditions and the following disclaimer.
81
+ * Redistributions in binary form must reproduce the above copyright
82
+ notice, this list of conditions and the following disclaimer in the
83
+ documentation and/or other materials provided with the distribution.
84
+ * Neither the name of Mark Rada nor the names of its
85
+ contributors may be used to endorse or promote products derived
86
+ from this software without specific prior written permission.
87
+
88
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
89
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
90
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
91
+ DISCLAIMED. IN NO EVENT SHALL Mark Rada BE LIABLE FOR ANY
92
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
93
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
94
+ GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
95
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
96
+ IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
97
+ OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
98
+ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,22 @@
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
+ unless RbConfig::CONFIG["CC"].match /clang/
10
+ clang = `which clang`.chomp
11
+ if clang.empty?
12
+ $stdout.puts "Clang not installed. Cannot build C extension"
13
+ raise "Clang not installed. Cannot build C extension"
14
+ else
15
+ RbConfig::MAKEFILE_CONFIG["CC"] = clang
16
+ RbConfig::MAKEFILE_CONFIG["CXX"] = clang
17
+ end
18
+ end
19
+ $CFLAGS << ' -DNOT_MACRUBY'
20
+ end
21
+
22
+ create_makefile('accessibility/key_coder')
@@ -0,0 +1,171 @@
1
+ /*
2
+ * key_coder.c
3
+ * KeyCoder
4
+ *
5
+ * Created by Mark Rada on 11-07-27.
6
+ * Copyright 2013 Mark Rada. All rights reserved.
7
+ * Copyright 2011-2012 Marketcircle Incorporated. All rights reserved.
8
+ */
9
+
10
+
11
+ #import <Cocoa/Cocoa.h>
12
+ #import <Carbon/Carbon.h>
13
+ #import <ApplicationServices/ApplicationServices.h>
14
+
15
+ #include "ruby.h"
16
+
17
+ static VALUE rb_cEventGenerator;
18
+ static ID sel_regenerate;
19
+
20
+ #ifdef NOT_MACRUBY
21
+ #define RELEASE(x) CFRelease(x)
22
+ #else
23
+ #define RELEASE(x) CFMakeCollectable(x)
24
+ #endif
25
+
26
+
27
+ /*
28
+ * Generate the mapping of characters to key codes for keys that can be
29
+ * remapped based on keyboard layout. Changing the keyboard layout at
30
+ * runtime will cause the returned hash to be different.
31
+ *
32
+ * @example
33
+ *
34
+ * KeyCoder.dynamic_mapping => { "a" => 0, "b" => 24, ... }
35
+ *
36
+ * @return [Hash{String=>Number}]
37
+ */
38
+
39
+ static
40
+ VALUE
41
+ keycoder_dynamic_mapping()
42
+ {
43
+
44
+ VALUE map = rb_hash_new();
45
+
46
+ #ifdef NOT_MACRUBY
47
+ @autoreleasepool {
48
+ #endif
49
+
50
+ TISInputSourceRef keyboard = TISCopyCurrentKeyboardLayoutInputSource();
51
+ CFDataRef layout_data = (CFDataRef)TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData);
52
+ const UCKeyboardLayout* layout = (const UCKeyboardLayout*)CFDataGetBytePtr(layout_data);
53
+
54
+ void (^key_coder)(int) = ^(int key_code) {
55
+ UniChar string[255];
56
+ UniCharCount string_length = 0;
57
+ UInt32 dead_key_state = 0;
58
+ UCKeyTranslate(
59
+ layout,
60
+ key_code,
61
+ kUCKeyActionDown,
62
+ 0,
63
+ LMGetKbdType(), // kb type
64
+ 0, // OptionBits keyTranslateOptions,
65
+ &dead_key_state,
66
+ 255,
67
+ &string_length,
68
+ string
69
+ );
70
+
71
+ NSString* nsstring = [NSString stringWithCharacters:string length:string_length];
72
+ rb_hash_aset(map, rb_str_new_cstr([nsstring UTF8String]), INT2FIX(key_code));
73
+ };
74
+
75
+ // skip 65-92 since they are hard coded and do not change
76
+ for (int key_code = 0; key_code < 65; key_code++)
77
+ key_coder(key_code);
78
+ for (int key_code = 93; key_code < 127; key_code++)
79
+ key_coder(key_code);
80
+
81
+ RELEASE(keyboard);
82
+
83
+ #ifdef NOT_MACRUBY
84
+ }; // Close the autorelease pool
85
+ #endif
86
+
87
+
88
+ return map;
89
+ }
90
+
91
+
92
+ /*
93
+ * Post the given event to the system and return `true`. This method
94
+ * will also add a small (9000 microsecond) delay after posting to
95
+ * ensure that keyboard actions do not go too fast.
96
+ *
97
+ * @example
98
+ *
99
+ * KeyCoder.post_event [0, true] -> true
100
+ *
101
+ * @param [Array(Number, Boolean)]
102
+ * @return [true]
103
+ */
104
+
105
+ static
106
+ VALUE
107
+ keycoder_post_event(VALUE self, VALUE event)
108
+ {
109
+ VALUE code = rb_ary_entry(event, 0);
110
+ VALUE state = rb_ary_entry(event, 1);
111
+
112
+ CGEventRef event_ref = CGEventCreateKeyboardEvent(NULL, FIX2LONG(code), state);
113
+ CGEventPost(kCGHIDEventTap, event_ref);
114
+ RELEASE(event_ref);
115
+
116
+ usleep(9000); // 9000 is a magic number
117
+ return Qtrue;
118
+ }
119
+
120
+
121
+ @interface AKEventGenerator : NSObject
122
+ - (void)regenerateDynamicMapping;
123
+ @end
124
+
125
+ @implementation AKEventGenerator
126
+
127
+ - (void)regenerateDynamicMapping
128
+ {
129
+ rb_funcall(rb_cEventGenerator, sel_regenerate, 0);
130
+ }
131
+
132
+ @end
133
+
134
+
135
+ void
136
+ Init_key_coder()
137
+ {
138
+ /*
139
+ * Document-class: KeyCoder
140
+ *
141
+ * Class that encapsulates some low level work for finding key code mappings
142
+ * and posting keyboard events to the system.
143
+ *
144
+ */
145
+ VALUE cKeyCoder = rb_define_class("KeyCoder", rb_cObject);
146
+ rb_define_singleton_method(cKeyCoder, "dynamic_mapping", keycoder_dynamic_mapping, 0);
147
+ rb_define_singleton_method(cKeyCoder, "post_event", keycoder_post_event, 1);
148
+
149
+
150
+
151
+ /* Register to be notified if the keyboard layout changes at runtime. The
152
+ * receiver will tell the event generator to regenerate its mapping.
153
+ * This will only work if a run loop is running (e.g. using the "spin"
154
+ * method that comes with accessibility_core).
155
+ */
156
+
157
+ VALUE rb_mAccessibility = rb_define_module("Accessibility");
158
+ VALUE rb_mKeyboard = rb_define_module_under(rb_mAccessibility, "Keyboard");
159
+ rb_cEventGenerator = rb_define_class_under(rb_mKeyboard, "EventGenerator", rb_cObject);
160
+
161
+ sel_regenerate = rb_intern("regenerate_dynamic_mapping");
162
+
163
+ static AKEventGenerator* dynamic_regenerator;
164
+ dynamic_regenerator = [[AKEventGenerator alloc] init];
165
+
166
+ [[NSDistributedNotificationCenter defaultCenter]
167
+ addObserver:dynamic_regenerator
168
+ selector:sel_registerName("regenerateDynamicMapping")
169
+ name:(NSString*)kTISNotifySelectedKeyboardInputSourceChanged
170
+ object:nil];
171
+ }
@@ -0,0 +1,34 @@
1
+ require 'accessibility/keyboard/version'
2
+ require 'accessibility/keyboard/parser'
3
+ require 'accessibility/keyboard/event_generator'
4
+
5
+
6
+ ##
7
+ # Parses strings of human readable text into a series of events
8
+ #
9
+ # Events are meant to be processed by {KeyCoder.post_event} or
10
+ # [Accessibility::Core#post](https://github.com/AXElements/accessibility_core/blob/master/lib/accessibility/core/macruby.rb#L597).
11
+ #
12
+ # Supports most, if not all, latin keyboard layouts, maybe some
13
+ # international layouts as well. Japanese layouts can be made to work with
14
+ # use of `String#transform` on MacRuby. See README for examples.
15
+ #
16
+ module Accessibility::Keyboard
17
+
18
+ ##
19
+ # Generate keyboard events for the given string
20
+ #
21
+ # Strings should be in a human readable format with a few exceptions.
22
+ # Command key (e.g. control, option, command) should be written in
23
+ # string as they appear in {Accessibility::Keyboard::EventGenerator::CUSTOM}.
24
+ #
25
+ # For more details on event generation, read the
26
+ # [Keyboarding wiki](http://github.com/AXElements/AXElements/wiki/Keyboarding).
27
+ #
28
+ # @param string [#to_s]
29
+ # @return [Array<Array(Fixnum,Boolean)>]
30
+ def keyboard_events_for string
31
+ EventGenerator.new(Parser.new(string).parse).generate
32
+ end
33
+
34
+ end
@@ -0,0 +1,351 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'accessibility/keyboard/version'
4
+ require 'accessibility/key_coder'
5
+
6
+ ##
7
+ # Generate a sequence of keyboard events given a sequence of tokens.
8
+ # The token format is defined by the
9
+ # {Accessibility::Keyboard::EventGenerator}
10
+ # class output; it is best to use that class to generate the tokens.
11
+ #
12
+ # @example
13
+ #
14
+ # # Upper case 'A'
15
+ # EventGenerator.new(["A"]).generate # => [[56,true],[70,true],[70,false],[56,false]]
16
+ #
17
+ # # Press the volume up key
18
+ # EventGenerator.new([["\\F12"]]).generate # => [[0x6F,true],[0x6F,false]]
19
+ #
20
+ # # Hotkey, press and hold command key and then 'a', then release both
21
+ # EventGenerator.new([["\\CMD",["a"]]]).generate # => [[55,true],[70,true],[70,false],[55,false]]
22
+ #
23
+ # # Press the return/enter key
24
+ # EventGenerator.new(["\n"]).generate # => [[10,true],[10,false]]
25
+ #
26
+ class Accessibility::Keyboard::EventGenerator
27
+
28
+ ##
29
+ # Regenerate the portion of the key mapping that is set dynamically
30
+ # based on keyboard layout (e.g. US, Dvorak, etc.).
31
+ #
32
+ # This method should be called whenever the keyboard layout changes.
33
+ # This can be called automatically by registering for a notification
34
+ # in a run looped environment.
35
+ def self.regenerate_dynamic_mapping
36
+ # KeyCoder is declared in the Objective-C extension
37
+ MAPPING.merge! KeyCoder.dynamic_mapping
38
+ # Also add an alias to the mapping
39
+ MAPPING["\n"] = MAPPING["\r"]
40
+ end
41
+
42
+ ##
43
+ # Dynamic mapping of characters to keycodes. The map is generated at
44
+ # startup time in order to support multiple keyboard layouts.
45
+ #
46
+ # @return [Hash{String=>Fixnum}]
47
+ MAPPING = {}
48
+
49
+ # Initialize the table
50
+ regenerate_dynamic_mapping
51
+
52
+ ##
53
+ # @note These mappings are all static and come from `/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h`
54
+ #
55
+ # Map of custom escape sequences to their hardcoded keycode value.
56
+ #
57
+ # @return [Hash{String=>Fixnum}]
58
+ CUSTOM = {
59
+ "\\FUNCTION" => 0x3F, # Standard Control Keys
60
+ "\\FN" => 0x3F,
61
+ "\\CONTROL" => 0x3B,
62
+ "\\CTRL" => 0x3B,
63
+ "\\OPTION" => 0x3A,
64
+ "\\OPT" => 0x3A,
65
+ "\\ALT" => 0x3A,
66
+ "\\COMMAND" => 0x37,
67
+ "\\CMD" => 0x37,
68
+ "\\LSHIFT" => 0x38,
69
+ "\\SHIFT" => 0x38,
70
+ "\\CAPSLOCK" => 0x39,
71
+ "\\CAPS" => 0x39,
72
+ "\\ROPTION" => 0x3D,
73
+ "\\ROPT" => 0x3D,
74
+ "\\RALT" => 0x3D,
75
+ "\\RCONTROL" => 0x3E,
76
+ "\\RCTRL" => 0x3E,
77
+ "\\RSHIFT" => 0x3C,
78
+ "\\ESCAPE" => 0x35, # Top Row Keys
79
+ "\\ESC" => 0x35,
80
+ "\\VOLUMEUP" => 0x48,
81
+ "\\VOLUP" => 0x48,
82
+ "\\VOLUMEDOWN" => 0x49,
83
+ "\\VOLDOWN" => 0x49,
84
+ "\\MUTE" => 0x4A,
85
+ "\\F1" => 0x7A,
86
+ "\\F2" => 0x78,
87
+ "\\F3" => 0x63,
88
+ "\\F4" => 0x76,
89
+ "\\F5" => 0x60,
90
+ "\\F6" => 0x61,
91
+ "\\F7" => 0x62,
92
+ "\\F8" => 0x64,
93
+ "\\F9" => 0x65,
94
+ "\\F10" => 0x6D,
95
+ "\\F11" => 0x67,
96
+ "\\F12" => 0x6F,
97
+ "\\F13" => 0x69,
98
+ "\\F14" => 0x6B,
99
+ "\\F15" => 0x71,
100
+ "\\F16" => 0x6A,
101
+ "\\F17" => 0x40,
102
+ "\\F18" => 0x4F,
103
+ "\\F19" => 0x50,
104
+ "\\F20" => 0x5A,
105
+ "\\HELP" => 0x72, # Island Keys
106
+ "\\HOME" => 0x73,
107
+ "\\END" => 0x77,
108
+ "\\PAGEUP" => 0x74,
109
+ "\\PAGEDOWN" => 0x79,
110
+ "\\DELETE" => 0x75,
111
+ "\\LEFT" => 0x7B, # Arrow Keys
112
+ "\\<-" => 0x7B,
113
+ "\\RIGHT" => 0x7C,
114
+ "\\->" => 0x7C,
115
+ "\\DOWN" => 0x7D,
116
+ "\\UP" => 0x7E,
117
+ "\\0" => 0x52, # Keypad Keys
118
+ "\\1" => 0x53,
119
+ "\\2" => 0x54,
120
+ "\\3" => 0x55,
121
+ "\\4" => 0x56,
122
+ "\\5" => 0x57,
123
+ "\\6" => 0x58,
124
+ "\\7" => 0x59,
125
+ "\\8" => 0x5B,
126
+ "\\9" => 0x5C,
127
+ "\\DECIMAL" => 0x41,
128
+ "\\." => 0x41,
129
+ "\\PLUS" => 0x45,
130
+ "\\+" => 0x45,
131
+ "\\MULTIPLY" => 0x43,
132
+ "\\*" => 0x43,
133
+ "\\MINUS" => 0x4E,
134
+ "\\-" => 0x4E,
135
+ "\\DIVIDE" => 0x4B,
136
+ "\\/" => 0x4B,
137
+ "\\EQUALS" => 0x51,
138
+ "\\=" => 0x51,
139
+ "\\ENTER" => 0x4C,
140
+ "\\CLEAR" => 0x47,
141
+ }
142
+
143
+ ##
144
+ # Mapping of shifted (characters written when holding shift) characters
145
+ # to keycodes.
146
+ #
147
+ # @return [Hash{String=>Fixnum}]
148
+ SHIFTED = {
149
+ '~' => '`',
150
+ '!' => '1',
151
+ '@' => '2',
152
+ '#' => '3',
153
+ '$' => '4',
154
+ '%' => '5',
155
+ '^' => '6',
156
+ '&' => '7',
157
+ '*' => '8',
158
+ '(' => '9',
159
+ ')' => '0',
160
+ '{' => '[',
161
+ '}' => ']',
162
+ '?' => '/',
163
+ '+' => '=',
164
+ '|' => "\\",
165
+ ':' => ';',
166
+ '_' => '-',
167
+ '"' => "'",
168
+ '<' => ',',
169
+ '>' => '.',
170
+ 'A' => 'a',
171
+ 'B' => 'b',
172
+ 'C' => 'c',
173
+ 'D' => 'd',
174
+ 'E' => 'e',
175
+ 'F' => 'f',
176
+ 'G' => 'g',
177
+ 'H' => 'h',
178
+ 'I' => 'i',
179
+ 'J' => 'j',
180
+ 'K' => 'k',
181
+ 'L' => 'l',
182
+ 'M' => 'm',
183
+ 'N' => 'n',
184
+ 'O' => 'o',
185
+ 'P' => 'p',
186
+ 'Q' => 'q',
187
+ 'R' => 'r',
188
+ 'S' => 's',
189
+ 'T' => 't',
190
+ 'U' => 'u',
191
+ 'V' => 'v',
192
+ 'W' => 'w',
193
+ 'X' => 'x',
194
+ 'Y' => 'y',
195
+ 'Z' => 'z',
196
+ }
197
+
198
+ ##
199
+ # Mapping of optioned (characters written when holding option/alt)
200
+ # characters to keycodes.
201
+ #
202
+ # @return [Hash{String=>Fixnum}]
203
+ OPTIONED = {
204
+ '¡' => '1',
205
+ '™' => '2',
206
+ '£' => '3',
207
+ '¢' => '4',
208
+ '∞' => '5',
209
+ '§' => '6',
210
+ '¶' => '7',
211
+ '•' => '8',
212
+ 'ª' => '9',
213
+ 'º' => '0',
214
+ '“' => '[',
215
+ '‘' => ']',
216
+ 'æ' => "'",
217
+ '≤' => ',',
218
+ '≥' => '.',
219
+ 'π' => 'p',
220
+ '¥' => 'y',
221
+ 'ƒ' => 'f',
222
+ '©' => 'g',
223
+ '®' => 'r',
224
+ '¬' => 'l',
225
+ '÷' => '/',
226
+ '≠' => '=',
227
+ '«' => "\\",
228
+ 'å' => 'a',
229
+ 'ø' => 'o',
230
+ '´' => 'e',
231
+ '¨' => 'u',
232
+ 'ˆ' => 'i',
233
+ '∂' => 'd',
234
+ '˙' => 'h',
235
+ '†' => 't',
236
+ '˜' => 'n',
237
+ 'ß' => 's',
238
+ '–' => '-',
239
+ '…' => ';',
240
+ 'œ' => 'q',
241
+ '∆' => 'j',
242
+ '˚' => 'k',
243
+ '≈' => 'x',
244
+ '∫' => 'b',
245
+ 'µ' => 'm',
246
+ '∑' => 'w',
247
+ '√' => 'v',
248
+ 'Ω' => 'z',
249
+ }
250
+
251
+
252
+ ##
253
+ # Once {#generate} is called, this contains the sequence of
254
+ # events.
255
+ #
256
+ # @return [Array<Array(Fixnum,Boolean)>]
257
+ attr_reader :events
258
+
259
+ # @param tokens [Array<String,Array<String,Array...>>]
260
+ def initialize tokens
261
+ @tokens = tokens
262
+ # *3 since the output array will be at least *2 the
263
+ # number of tokens passed in, but will often be larger
264
+ # due to shifted/optioned characters and custom escapes;
265
+ # though a better number could be derived from
266
+ # analyzing common input...
267
+ @events = Array.new tokens.size*3
268
+ end
269
+
270
+ ##
271
+ # Generate the events for the tokens the event generator
272
+ # was initialized with. Returns the generated events, though
273
+ # you can also use {#events} to get the events later.
274
+ #
275
+ # @return [Array<Array(Fixnum,Boolean)>]
276
+ def generate
277
+ @index = 0
278
+ gen_all @tokens
279
+ @events.compact!
280
+ @events
281
+ end
282
+
283
+
284
+ private
285
+
286
+ def add event
287
+ @events[@index] = event
288
+ @index += 1
289
+ end
290
+ def previous_token; @events[@index-1] end
291
+ def rewind_index; @index -= 1 end
292
+
293
+ def gen_all tokens
294
+ tokens.each do |token|
295
+ if token.kind_of? Array
296
+ gen_nested token.first, token[1..-1]
297
+ else
298
+ gen_single token
299
+ end
300
+ end
301
+ end
302
+
303
+ def gen_nested head, tail
304
+ ((code = CUSTOM[head]) && gen_dynamic(code, tail)) ||
305
+ ((code = MAPPING[head]) && gen_dynamic(code, tail)) ||
306
+ ((code = SHIFTED[head]) && gen_shifted(code, tail)) ||
307
+ ((code = OPTIONED[head]) && gen_optioned(code, tail)) ||
308
+ gen_all(head.split(EMPTY_STRING)) # handling a special case :(
309
+ end
310
+
311
+ def gen_single token
312
+ ((code = MAPPING[token]) && gen_dynamic(code, nil)) ||
313
+ ((code = SHIFTED[token]) && gen_shifted(code, nil)) ||
314
+ ((code = OPTIONED[token]) && gen_optioned(code, nil)) ||
315
+ raise(ArgumentError, "#{token.inspect} has no mapping, bail!")
316
+ end
317
+
318
+ def gen_shifted code, tail
319
+ previous_token == SHIFT_UP ? rewind_index : add(SHIFT_DOWN)
320
+ gen_dynamic MAPPING[code], tail
321
+ add SHIFT_UP
322
+ end
323
+
324
+ def gen_optioned code, tail
325
+ previous_token == OPTION_UP ? rewind_index : add(OPTION_DOWN)
326
+ gen_dynamic MAPPING[code], tail
327
+ add OPTION_UP
328
+ end
329
+
330
+ def gen_dynamic code, tail
331
+ add [code, true]
332
+ gen_all tail if tail
333
+ add [code, false]
334
+ end
335
+
336
+ # @private
337
+ # @return [String]
338
+ EMPTY_STRING = ""
339
+ # @private
340
+ # @return [Array(Number,Boolean)]
341
+ OPTION_DOWN = [58, true]
342
+ # @private
343
+ # @return [Array(Number,Boolean)]
344
+ OPTION_UP = [58, false]
345
+ # @private
346
+ # @return [Array(Number,Boolean)]
347
+ SHIFT_DOWN = [56, true]
348
+ # @private
349
+ # @return [Array(Number,Boolean)]
350
+ SHIFT_UP = [56, false]
351
+ end
@@ -0,0 +1,111 @@
1
+ require 'accessibility/keyboard/version'
2
+
3
+ ##
4
+ # Parse arbitrary strings into a stream of keyboard events
5
+ #
6
+ # This class will take a string and break it up into chunks for the keyboard
7
+ # event generator. The structure generated here is an array that contains
8
+ # strings and recursively other arrays of strings and arrays of strings.
9
+ #
10
+ # @example
11
+ #
12
+ # Parser.new("Hai").parse # => ['H','a','i']
13
+ # Parser.new("\\CONTROL").parse # => [["\\CONTROL"]]
14
+ # Parser.new("\\COMMAND+a").parse # => [["\\COMMAND", ['a']]]
15
+ # Parser.new("One\nTwo").parse # => ['O','n','e',"\n",'T','w','o']
16
+ #
17
+ class Accessibility::Keyboard::Parser
18
+
19
+ ##
20
+ # Once a string is parsed, this contains the tokenized structure.
21
+ #
22
+ # @return [Array<String,Array<String,...>]
23
+ attr_accessor :tokens
24
+
25
+ # @param string [String,#to_s]
26
+ def initialize string
27
+ @chars = string.to_s
28
+ @tokens = []
29
+ end
30
+
31
+ ##
32
+ # Tokenize the string that the parser was initialized with and
33
+ # return the sequence of tokens that were parsed.
34
+ #
35
+ # @return [Array<String,Array<String,...>]
36
+ def parse
37
+ length = @chars.length
38
+ @index = 0
39
+ while @index < length
40
+ @tokens << if custom?
41
+ parse_custom
42
+ else
43
+ parse_char
44
+ end
45
+ @index += 1
46
+ end
47
+ @tokens
48
+ end
49
+
50
+
51
+ private
52
+
53
+ ##
54
+ # Is it a real custom escape? Kind of a lie, there is one
55
+ # case it does not handle--they get handled in the generator,
56
+ # but maybe they should be handled here?
57
+ # - An upper case letter or symbol following `"\\"` that is
58
+ # not mapped
59
+ def custom?
60
+ @chars[@index] == CUSTOM_ESCAPE &&
61
+ (next_char = @chars[@index+1]) &&
62
+ next_char == next_char.upcase &&
63
+ next_char != SPACE
64
+ end
65
+
66
+ # @todo refactor
67
+ # @return [Array]
68
+ def parse_custom
69
+ start = @index
70
+ loop do
71
+ char = @chars[@index]
72
+ if char == PLUS
73
+ if @chars[@index-1] == CUSTOM_ESCAPE # \\+ case
74
+ @index += 1
75
+ return custom_subseq start
76
+ else
77
+ tokens = custom_subseq start
78
+ @index += 1
79
+ return tokens << parse_custom
80
+ end
81
+ elsif char == SPACE
82
+ return custom_subseq start
83
+ elsif char == nil
84
+ raise ArgumentError, "Bad escape sequence" if start == @index
85
+ return custom_subseq start
86
+ else
87
+ @index += 1
88
+ end
89
+ end
90
+ end
91
+
92
+ # @return [Array]
93
+ def custom_subseq start
94
+ [@chars[start...@index]]
95
+ end
96
+
97
+ # @return [String]
98
+ def parse_char
99
+ @chars[@index]
100
+ end
101
+
102
+ # @private
103
+ # @return [String]
104
+ SPACE = " "
105
+ # @private
106
+ # @return [String]
107
+ PLUS = "+"
108
+ # @private
109
+ # @return [String]
110
+ CUSTOM_ESCAPE = "\\"
111
+ end
@@ -0,0 +1,8 @@
1
+ ##
2
+ # Namespace for accessibility related objects
3
+ module Accessibility
4
+ module Keyboard
5
+ # @return [String]
6
+ VERSION = '1.0.0'
7
+ end
8
+ end
@@ -0,0 +1,149 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'test/helper'
3
+ require 'accessibility/keyboard/event_generator'
4
+
5
+ class TestEventGenerator < MiniTest::Unit::TestCase
6
+
7
+ def gen tokens
8
+ Accessibility::Keyboard::EventGenerator.new(tokens).generate
9
+ end
10
+
11
+ def map; @@map ||= KeyCoder.dynamic_mapping; end
12
+
13
+ def t; true; end
14
+ def f; false; end
15
+
16
+ def a; @@a ||= map['a']; end
17
+ def c; @@c ||= map['c']; end
18
+ def e; @@e ||= map['e']; end
19
+ def h; @@h ||= map['h']; end
20
+ def i; @@i ||= map['i']; end
21
+ def k; @@k ||= map['k']; end
22
+ def m; @@m ||= map["m"]; end
23
+
24
+ def two; @@two ||= map['2']; end
25
+ def four; @@four ||= map['4']; end
26
+
27
+ def retern; @@retern ||= map["\r"]; end
28
+ def tab; @@tab ||= map["\t"]; end
29
+ def space; @@space ||= map["\s"]; end
30
+
31
+ def dash; @@dash ||= map["-"]; end
32
+ def comma; @@comma ||= map[","]; end
33
+ def apos; @@apos ||= map["'"]; end
34
+ def at; @@at ||= map["2"]; end
35
+ def paren; @@paren ||= map["9"]; end
36
+ def chev; @@chev ||= map["."]; end
37
+
38
+ def sigma; @@sigma ||= map["w"]; end
39
+ def tm; @@tm ||= map["2"]; end
40
+ def gbp; @@gbp ||= map["3"]; end
41
+ def omega; @@omega ||= map["z"]; end
42
+
43
+ def bslash; @@blash ||= map["\\"]; end
44
+
45
+ # key code for the left shift key
46
+ def sd; [56,t]; end
47
+ def su; [56,f]; end
48
+
49
+ # key code for the left option key
50
+ def od; [58,t]; end
51
+ def ou; [58,f]; end
52
+
53
+ # key code for the left command key
54
+ def cd; [0x37,t]; end
55
+ def cu; [0x37,f]; end
56
+
57
+ # key code for right arrow key
58
+ def rd; [0x7c,t]; end
59
+ def ru; [0x7c,f]; end
60
+
61
+ # key code for left control key
62
+ def ctrld; [0x3B,t]; end
63
+ def ctrlu; [0x3B,f]; end
64
+
65
+
66
+ def test_generate_lowercase
67
+ assert_equal [[a,t],[a,f]], gen(['a'])
68
+ assert_equal [[c,t],[c,f],[k,t],[k,f]], gen(['c','k'])
69
+ assert_equal [[e,t],[e,f],[e,t],[e,f]], gen(['e','e'])
70
+ assert_equal [[c,t],[c,f],[a,t],[a,f],[k,t],[k,f],[e,t],[e,f]], gen(['c','a','k','e'])
71
+ end
72
+
73
+ def test_generate_uppercase
74
+ assert_equal [sd,[a,t],[a,f],su], gen(['A'])
75
+ assert_equal [sd,[c,t],[c,f],[k,t],[k,f],su], gen(['C','K'])
76
+ assert_equal [sd,[e,t],[e,f],[e,t],[e,f],su], gen(['E','E'])
77
+ assert_equal [sd,[c,t],[c,f],[a,t],[a,f],[k,t],[k,f],su], gen(['C','A','K'])
78
+ end
79
+
80
+ def test_generate_numbers
81
+ assert_equal [[two,t],[two,f]], gen(['2'])
82
+ assert_equal [[four,t],[four,f],[two,t],[two,f]], gen(['4','2'])
83
+ assert_equal [[two,t],[two,f],[two,t],[two,f]], gen(['2','2'])
84
+ end
85
+
86
+ def test_generate_ruby_escapes
87
+ assert_equal [[retern,t],[retern,f]], gen(["\r"])
88
+ assert_equal [[retern,t],[retern,f]], gen(["\n"])
89
+ assert_equal [[tab,t],[tab,f]], gen(["\t"])
90
+ assert_equal [[space,t],[space,f]], gen(["\s"])
91
+ assert_equal [[space,t],[space,f]], gen([" "])
92
+ end
93
+
94
+ def test_generate_symbols
95
+ assert_equal [[dash,t],[dash,f]], gen(["-"])
96
+ assert_equal [[comma,t],[comma,f]], gen([","])
97
+ assert_equal [[apos,t],[apos,f]], gen(["'"])
98
+ assert_equal [sd,[at,t],[at,f],su], gen(["@"])
99
+ assert_equal [sd,[paren,t],[paren,f],su], gen(["("])
100
+ assert_equal [sd,[chev,t],[chev,f],su], gen([">"])
101
+ end
102
+
103
+ def test_generate_unicode # holding option
104
+ assert_equal [od,[sigma,t],[sigma,f],ou], gen(["∑"])
105
+ assert_equal [od,[tm,t],[tm,f],ou], gen(["™"])
106
+ assert_equal [od,[gbp,t],[gbp,f],ou], gen(["£"])
107
+ assert_equal [od,[omega,t],[omega,f],ou], gen(["Ω"])
108
+ assert_equal [od,[tm,t],[tm,f],[gbp,t],[gbp,f],ou], gen(["™","£"])
109
+ end
110
+
111
+ def test_generate_backslashes
112
+ assert_equal [[bslash,t],[bslash,f]], gen(["\\"])
113
+ assert_equal [[bslash,t],[bslash,f],[space,t],[space,f]], gen(["\\"," "])
114
+ assert_equal [[bslash,t],[bslash,f],[h,t],[h,f],[m,t],[m,f]], gen(["\\",'h','m'])
115
+ # is this the job of the parser or the lexer?
116
+ assert_equal [[bslash,t],[bslash,f],sd,[h,t],[h,f],[m,t],[m,f],su], gen([["\\HM"]])
117
+ end
118
+
119
+ def test_generate_a_custom_escape
120
+ assert_equal [cd,cu], gen([["\\COMMAND"]])
121
+ assert_equal [cd,cu], gen([["\\CMD"]])
122
+ assert_equal [ctrld,ctrlu], gen([["\\CONTROL"]])
123
+ assert_equal [ctrld,ctrlu], gen([["\\CTRL"]])
124
+ end
125
+
126
+ def test_generate_hotkey
127
+ assert_equal [ctrld,[a,t],[a,f],ctrlu], gen([["\\CONTROL",["a"]]])
128
+ assert_equal [cd,sd,rd,ru,su,cu], gen([["\\COMMAND",['\SHIFT',['\->']]]])
129
+ end
130
+
131
+ def test_generate_real_use # a regression
132
+ assert_equal [ctrld,[a,t],[a,f],ctrlu,[h,t],[h,f]], gen([["\\CTRL",["a"]],"h"])
133
+ end
134
+
135
+ def test_bails_for_unmapped_token
136
+ # cannot generate snowmen :(
137
+ e = assert_raises(ArgumentError) { gen(["☃"]) }
138
+ assert_match /bail/i, e.message
139
+ end
140
+
141
+ def test_generate_arbitrary_nested_array_sequence
142
+ assert_equal [[c,t],[a,t],[k,t],[e,t],[e,f],[k,f],[a,f],[c,f]], gen([["c",["a",["k",["e"]]]]])
143
+ end
144
+
145
+ def test_generate_command_A
146
+ assert_equal [cd,sd,[a,t],[a,f],su,cu], gen([["\\COMMAND",["A"]]])
147
+ end
148
+
149
+ end
@@ -0,0 +1,3 @@
1
+ gem 'minitest'
2
+ require 'minitest/autorun'
3
+ require 'minitest/pride'
@@ -0,0 +1,29 @@
1
+ require 'test/helper'
2
+ require 'accessibility/keyboard'
3
+
4
+ # @note DO NOT TEST POSTING EVENTS HERE
5
+ # We only want to test posting events if all the tests in this file pass,
6
+ # otherwise the posted events may be unpredictable depending on what fails.
7
+ # Test event posting in the integration tests.
8
+ class TestKeyboard < MiniTest::Unit::TestCase
9
+ include Accessibility::Keyboard
10
+
11
+ # basic test to make sure the lexer and generator get along
12
+ def test_keyboard_events_for
13
+ events = keyboard_events_for 'cheezburger'
14
+ assert_kind_of Array, events
15
+ refute_empty events
16
+
17
+ assert_equal true, events[0][1]
18
+ assert_equal false, events[1][1]
19
+ end
20
+
21
+ def test_dynamic_map_initialized
22
+ refute_empty Accessibility::Keyboard::EventGenerator::MAPPING
23
+ end
24
+
25
+ def test_can_parse_empty_string
26
+ assert_equal [], keyboard_events_for('')
27
+ end
28
+
29
+ end
@@ -0,0 +1,62 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'test/helper'
3
+ require 'accessibility/keyboard/parser'
4
+
5
+ class TestParser < MiniTest::Unit::TestCase
6
+
7
+ def parse string
8
+ Accessibility::Keyboard::Parser.new(string).parse
9
+ end
10
+
11
+ def test_parse_simple_string
12
+ assert_equal [], parse('')
13
+ assert_equal ['"',"J","u","s","t"," ","W","o","r","k","s",'"',"™"], parse('"Just Works"™')
14
+ assert_equal ["M","i","l","k",","," ","s","h","a","k","e","."], parse("Milk, shake.")
15
+ assert_equal ["D","B","7"], parse("DB7")
16
+ end
17
+
18
+ def test_parse_single_custom_escape
19
+ assert_equal [["\\CMD"]], parse("\\CMD")
20
+ assert_equal [["\\1"]], parse("\\1")
21
+ assert_equal [["\\F1"]], parse("\\F1")
22
+ assert_equal [["\\*"]], parse("\\*")
23
+ end
24
+
25
+ def test_parse_hotkey_custom_escape
26
+ assert_equal [["\\COMMAND",[","]]], parse("\\COMMAND+,")
27
+ assert_equal [["\\COMMAND",["\\SHIFT",["s"]]]], parse("\\COMMAND+\\SHIFT+s")
28
+ assert_equal [["\\COMMAND",["\\+"]]], parse("\\COMMAND+\\+")
29
+ assert_equal [["\\FN",["\\F10"]]], parse("\\FN+\\F10")
30
+ end
31
+
32
+ def test_parse_ruby_escapes
33
+ assert_equal ["\n","\r","\t","\b"], parse("\n\r\t\b")
34
+ assert_equal ["O","n","e","\n","T","w","o"], parse("One\nTwo")
35
+ assert_equal ["L","i","e","\b","\b","\b","d","e","l","i","s","h"], parse("Lie\b\b\bdelish")
36
+ end
37
+
38
+ def test_parse_compparse_string
39
+ assert_equal ["T","e","s","t",["\\CMD",["s"]]], parse("Test\\CMD+s")
40
+ assert_equal ["Z","O","M","G"," ","1","3","3","7","!","!","1"], parse("ZOMG 1337!!1")
41
+ assert_equal ["F","u","u","!","@","#","%",["\\CMD",["a"]],"\b"], parse("Fuu!@#%\\CMD+a \b")
42
+ assert_equal [["\\CMD",["a"]],"\b","A","l","l"," ","g","o","n","e","!"], parse("\\CMD+a \bAll gone!")
43
+ end
44
+
45
+ def test_parse_backslash # make sure we handle these edge cases predictably
46
+ assert_equal ["\\"], parse("\\")
47
+ assert_equal ["\\"," "], parse("\\ ")
48
+ assert_equal ["\\","h","m","m"], parse("\\hmm")
49
+ assert_equal [["\\HMM"]], parse("\\HMM") # the one missed case
50
+ end
51
+
52
+ def test_parse_plus_escape
53
+ assert_equal [["\\+"]], parse("\\+")
54
+ end
55
+
56
+ def test_parse_bad_custom_escape_sequence
57
+ assert_raises ArgumentError do
58
+ parse("\\COMMAND+")
59
+ end
60
+ end
61
+
62
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH << File.expand_path('../../', __FILE__)
4
+ $LOAD_PATH << File.expand_path('../../lib', __FILE__)
5
+ require 'test/helper'
6
+
7
+ ARGV.each do |file|
8
+ if file == '-v' || file == '--verbose'
9
+ $VERBOSE = true
10
+ else
11
+ require file
12
+ end
13
+ end
14
+
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: accessibility_keyboard
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Mark Rada
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-02-03 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: ! 'Simulate keyboard input via the Mac OS X Accessibility Framework.
15
+ This
16
+
17
+ gem is a component of AXElements.
18
+
19
+ '
20
+ email: mrada@marketcircle.com
21
+ executables: []
22
+ extensions:
23
+ - ext/key_coder/extconf.rb
24
+ extra_rdoc_files:
25
+ - README.markdown
26
+ - History.markdown
27
+ - CONTRIBUTING.markdown
28
+ - .yardopts
29
+ files:
30
+ - lib/accessibility/keyboard.rb
31
+ - lib/accessibility/keyboard/version.rb
32
+ - lib/accessibility/keyboard/parser.rb
33
+ - lib/accessibility/keyboard/event_generator.rb
34
+ - ext/key_coder/key_coder.c
35
+ - ext/key_coder/extconf.rb
36
+ - test/keyboard_test.rb
37
+ - test/parser_test.rb
38
+ - test/event_generator_test.rb
39
+ - test/helper.rb
40
+ - test/runner
41
+ - README.markdown
42
+ - History.markdown
43
+ - CONTRIBUTING.markdown
44
+ - .yardopts
45
+ homepage: http://github.com/AXElements/accessibility_keyboard
46
+ licenses:
47
+ - BSD 3-clause
48
+ post_install_message:
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ! '>='
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ segments:
59
+ - 0
60
+ hash: -4062551165478306694
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ! '>='
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ segments:
68
+ - 0
69
+ hash: -4062551165478306694
70
+ requirements: []
71
+ rubyforge_project:
72
+ rubygems_version: 1.8.24
73
+ signing_key:
74
+ specification_version: 3
75
+ summary: Keyboard simulation for OS X
76
+ test_files:
77
+ - test/keyboard_test.rb
78
+ - test/parser_test.rb
79
+ - test/event_generator_test.rb
80
+ - test/helper.rb
81
+ - test/runner