accessibility_keyboard 1.0.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.
@@ -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