| OLD | NEW |
| (Empty) | |
| 1 #!/usr/bin/python |
| 2 # |
| 3 # Copyright (c) 2010 The Chromium OS Authors. All rights reserved. |
| 4 # Use of this source code is governed by a BSD-style license that can be |
| 5 # found in the LICENSE file. |
| 6 |
| 7 import re |
| 8 import time |
| 9 |
| 10 import Xlib.display |
| 11 import Xlib.protocol.request |
| 12 from Xlib import X |
| 13 from Xlib import XK |
| 14 from Xlib.ext import xtest |
| 15 |
| 16 class AutoX(object): |
| 17 """AutoX provides an interface for interacting with X applications. |
| 18 |
| 19 This is done by using the XTEST extension to inject events into the X |
| 20 server. |
| 21 |
| 22 Example usage: |
| 23 |
| 24 autox = AutoX() |
| 25 autox.move_pointer(200, 100) |
| 26 autox.press_button(1) |
| 27 autox.move_pointer(250, 150) |
| 28 autox.release_button(1) |
| 29 |
| 30 autox.send_hotkey("Ctrl+Alt+L") |
| 31 autox.send_text("this is my password\n") |
| 32 """ |
| 33 |
| 34 # Map of characters that can be passed to send_text() that differ |
| 35 # from their X keysym names. |
| 36 __chars_to_keysyms = { |
| 37 ' ': 'space', |
| 38 '\n': 'Return', |
| 39 '\t': 'Tab', |
| 40 '~': 'asciitilde', |
| 41 '!': 'exclam', |
| 42 '@': 'at', |
| 43 '#': 'numbersign', |
| 44 '$': 'dollar', |
| 45 '%': 'percent', |
| 46 '^': 'asciicircum', |
| 47 '&': 'ampersand', |
| 48 '*': 'asterisk', |
| 49 '(': 'parenleft', |
| 50 ')': 'parenright', |
| 51 '-': 'minus', |
| 52 '_': 'underscore', |
| 53 '+': 'plus', |
| 54 '=': 'equal', |
| 55 '{': 'braceleft', |
| 56 '[': 'bracketleft', |
| 57 '}': 'braceright', |
| 58 ']': 'bracketright', |
| 59 '|': 'bar', |
| 60 ':': 'colon', |
| 61 ';': 'semicolon', |
| 62 '"': 'quotedbl', |
| 63 '\'': 'apostrophe', |
| 64 ',': 'comma', |
| 65 '<': 'less', |
| 66 '.': 'period', |
| 67 '>': 'greater', |
| 68 '/': 'slash', |
| 69 '?': 'question', |
| 70 } |
| 71 |
| 72 class Error(Exception): |
| 73 """Base exception class for AutoX.""" |
| 74 pass |
| 75 |
| 76 class RuntimeError(Error): |
| 77 """Error caused by a (possibly temporary) condition at runtime.""" |
| 78 pass |
| 79 |
| 80 class InputError(Error): |
| 81 """Error caused by invalid input from the caller.""" |
| 82 pass |
| 83 |
| 84 class InvalidKeySymError(InputError): |
| 85 """Error caused by the caller referencing an invalid keysym.""" |
| 86 def __init__(self, keysym): |
| 87 self.__keysym = keysym |
| 88 |
| 89 def __str__(self): |
| 90 return "Invalid keysym \"%s\"" % self.__keysym |
| 91 |
| 92 def __init__(self, display_name=None): |
| 93 self.__display = Xlib.display.Display(display_name) |
| 94 self.__root = self.__display.screen().root |
| 95 |
| 96 def __get_keycode_for_keysym(self, keysym): |
| 97 """Get the keycode corresponding to a keysym. |
| 98 |
| 99 Args: |
| 100 keysym: keysym name as str |
| 101 |
| 102 Returns: |
| 103 integer keycode |
| 104 |
| 105 Raises: |
| 106 InvalidKeySymError: keysym name isn't an actual keycode |
| 107 RuntimeError: unable to map the keysym to a keycode (maybe it |
| 108 isn't present in the current keymap) |
| 109 """ |
| 110 keysym_num = XK.string_to_keysym(keysym) |
| 111 if keysym_num == XK.NoSymbol: |
| 112 raise self.InvalidKeySymError(keysym) |
| 113 keycode = self.__display.keysym_to_keycode(keysym_num) |
| 114 if not keycode: |
| 115 raise self.RuntimeError( |
| 116 'Unable to map keysym "%s" to a keycode' % keysym) |
| 117 return keycode |
| 118 |
| 119 def __keysym_requires_shift(self, keysym): |
| 120 """Does a keysym require that a shift key be held down? |
| 121 |
| 122 Args: |
| 123 keysym: keysym name as str |
| 124 |
| 125 Returns: |
| 126 True or False |
| 127 |
| 128 Raises: |
| 129 InvalidKeySymError: keysym name isn't an actual keycode |
| 130 RuntimeError: unable to map the keysym to a keycode (maybe it |
| 131 isn't present in the current keymap) |
| 132 """ |
| 133 keysym_num = XK.string_to_keysym(keysym) |
| 134 if keysym_num == XK.NoSymbol: |
| 135 raise self.InvalidKeySymError(keysym) |
| 136 # This gives us a list of (keycode, index) tuples, sorted by index and |
| 137 # then by keycode. Index 0 is without any modifiers, 1 is with Shift, |
| 138 # 2 is with Mode_switch, and 3 is Mode_switch and Shift. |
| 139 keycodes = self.__display.keysym_to_keycodes(keysym_num) |
| 140 if not keycodes: |
| 141 raise self.RuntimeError( |
| 142 'Unable to map keysym "%s" to a keycode' % keysym) |
| 143 # We don't use Mode_switch for anything, at least currently, so just |
| 144 # check if the first index is unshifted. |
| 145 return keycodes[0][1] != 0 |
| 146 |
| 147 def __handle_key_command(keysym, key_press): |
| 148 """Looks up the keycode for a keysym and presses or releases it. |
| 149 |
| 150 Helper method for press_key() and release_key(). |
| 151 |
| 152 Args: |
| 153 keysym: keysym name as str |
| 154 key_press: True to send key press; False to send release |
| 155 |
| 156 Raises: |
| 157 InputError: input was invalid; details in exception |
| 158 InvalidKeySymError, RuntimeError: see __get_keycode_for_keysym() |
| 159 """ |
| 160 keycode = self.__get_keycode_for_keysym(keysym) |
| 161 if self.__keysym_requires_shift(keysym): |
| 162 raise self.InputError( |
| 163 'Keysym "%s" requires the Shift key to be held. Either use ' |
| 164 'send_text() or make separate calls to press/release_key(), ' |
| 165 'one for Shift_L and then one for the keycode\'s non-shifted ' |
| 166 'keysym' % keysym) |
| 167 |
| 168 type = X.KeyPress if key_press else X.KeyRelease |
| 169 xtest.fake_input(self.__display, type, detail=keycode) |
| 170 self.__display.sync() |
| 171 |
| 172 def __convert_escaped_string_to_keysym(self, escaped_string): |
| 173 """Read an escaped keysym name from the beginning of a string. |
| 174 |
| 175 Helper method called by send_text(). |
| 176 |
| 177 Args: |
| 178 escaped_string: str prefixed with a backslash followed by a |
| 179 keysym name in parens, e.g. "\\(Return)more text" |
| 180 |
| 181 Returns: |
| 182 tuple consisting of the keysym name and the number of |
| 183 characters that should be skipped to get to the next character |
| 184 in the string (including the leading backslash). For example, |
| 185 "\\(Space)blah" yields ("Space", 8). |
| 186 |
| 187 Raises: |
| 188 InputError: unable to find an escaped keysym-looking thing at |
| 189 the beginning of the string |
| 190 """ |
| 191 if escaped_string[0] != '\\': |
| 192 raise self.InputError('Escaped string is missing backslash') |
| 193 if len(escaped_string) < 2: |
| 194 raise self.InputError('Escaped string is too short') |
| 195 if escaped_string[1] == '\\': |
| 196 return ('backslash', 2) |
| 197 if escaped_string[1] != '(': |
| 198 raise self.InputError('Escaped string is missing opening paren') |
| 199 |
| 200 end_index = escaped_string.find(')') |
| 201 if end_index == -1 or end_index == 2: |
| 202 raise self.InputError('Escaped string is missing closing paren') |
| 203 return (escaped_string[2:end_index], end_index + 1) |
| 204 |
| 205 def __convert_char_to_keysym(self, char): |
| 206 """Convert a character into its keysym name. |
| 207 |
| 208 Args: |
| 209 char: str of length 1 containing the character to be looked up |
| 210 |
| 211 Returns: |
| 212 keysym name as str |
| 213 |
| 214 Raises: |
| 215 InputError: received non-length-1 string |
| 216 InvalidKeySymError: character wasn't a keysym that we know about |
| 217 (this may just mean that it needs to be added to |
| 218 '__chars_to_keysyms') |
| 219 """ |
| 220 if len(char) != 1: |
| 221 raise self.InputError('Got non-length-1 string "%s"' % char) |
| 222 if char.isalnum(): |
| 223 # Letters and digits are easy. |
| 224 return char |
| 225 if char in AutoX.__chars_to_keysyms: |
| 226 return AutoX.__chars_to_keysyms[char] |
| 227 raise self.InvalidKeySymError(char) |
| 228 |
| 229 def get_pointer_position(self): |
| 230 """Get the pointer's absolute position. |
| 231 |
| 232 Returns: |
| 233 (x, y) integer tuple |
| 234 """ |
| 235 reply = Xlib.protocol.request.QueryPointer( |
| 236 display=self.__display.display, window=self.__root) |
| 237 return (reply.root_x, reply.root_y) |
| 238 |
| 239 def press_button(self, button): |
| 240 """Press a mouse button. |
| 241 |
| 242 Args: |
| 243 button: 1-indexed mouse button to press |
| 244 """ |
| 245 xtest.fake_input(self.__display, X.ButtonPress, detail=button) |
| 246 self.__display.sync() |
| 247 |
| 248 def release_button(self, button): |
| 249 """Release a mouse button. |
| 250 |
| 251 Args: |
| 252 button: 1-indexed mouse button to release |
| 253 """ |
| 254 xtest.fake_input(self.__display, X.ButtonRelease, detail=button) |
| 255 self.__display.sync() |
| 256 |
| 257 def move_pointer(self, x, y): |
| 258 """Move the mouse pointer to an absolute position. |
| 259 |
| 260 Args: |
| 261 x, y: integer position relative to the root window's origin |
| 262 """ |
| 263 xtest.fake_input(self.__display, X.MotionNotify, x=x, y=y) |
| 264 self.__display.sync() |
| 265 |
| 266 def send_hotkey(self, hotkey): |
| 267 """Send a combination of keystrokes. |
| 268 |
| 269 Args: |
| 270 hotkey: str describing a '+' or '-'-separated sequence of |
| 271 keysyms, e.g. "Control_L+Alt_L+R" or "Ctrl-J". Several |
| 272 aliases are accepted: |
| 273 |
| 274 Ctrl -> Control_L |
| 275 Alt -> Alt_L |
| 276 Shift -> Shift_L |
| 277 |
| 278 Whitespace is permitted around individual keysyms. |
| 279 |
| 280 Raises: |
| 281 InputError: hotkey sequence contained an error |
| 282 InvalidKeySymError, RuntimeError: see __get_keycode_for_keysym() |
| 283 """ |
| 284 # Did the shift key occur in the combination? |
| 285 saw_shift = False |
| 286 keycodes = [] |
| 287 |
| 288 regexp = re.compile('[-+]') |
| 289 for keysym in regexp.split(hotkey): |
| 290 keysym = keysym.strip() |
| 291 |
| 292 if keysym == 'Ctrl': |
| 293 keysym = 'Control_L' |
| 294 elif keysym == 'Alt': |
| 295 keysym = 'Alt_L' |
| 296 elif keysym == 'Shift': |
| 297 keysym = 'Shift_L' |
| 298 |
| 299 if keysym == 'Shift_L' or keysym == 'Shift_R': |
| 300 saw_shift = True |
| 301 |
| 302 keycode = self.__get_keycode_for_keysym(keysym) |
| 303 |
| 304 # Bail if we're being asked to press a key that requires Shift and |
| 305 # the Shift key wasn't pressed already (but let it slide if they're |
| 306 # just asking for an uppercase letter). |
| 307 if self.__keysym_requires_shift(keysym) and not saw_shift and \ |
| 308 (len(keysym) != 1 or keysym < 'A' or keysym > 'Z'): |
| 309 raise self.InputError( |
| 310 'Keysym "%s" requires the Shift key to be held, ' |
| 311 'but it wasn\'t seen earlier in the key combo. ' |
| 312 'Either press Shift first or using the keycode\'s ' |
| 313 'non-shifted keysym instead' % keysym) |
| 314 |
| 315 keycodes.append(keycode) |
| 316 |
| 317 # Press the keys in the correct order and then reverse them in the |
| 318 # opposite order. |
| 319 for keycode in keycodes: |
| 320 xtest.fake_input(self.__display, X.KeyPress, detail=keycode) |
| 321 for keycode in reversed(keycodes): |
| 322 xtest.fake_input(self.__display, X.KeyRelease, detail=keycode) |
| 323 self.__display.sync() |
| 324 |
| 325 def press_key(self, keysym): |
| 326 """Press the key corresponding to a keysym. |
| 327 |
| 328 Args: |
| 329 keysym: keysym name as str |
| 330 """ |
| 331 self.__handle_key_command(keysym, True) # key_press=True |
| 332 |
| 333 def release_key(self, keysym): |
| 334 """Release the key corresponding to a keysym. |
| 335 |
| 336 Args: |
| 337 keysym: keysym name as str |
| 338 """ |
| 339 self.__handle_key_command(keysym, False) # key_press=False |
| 340 |
| 341 def send_text(self, text): |
| 342 """Type a sequence of characters. |
| 343 |
| 344 Args: |
| 345 text: sequence of characters to type. Along with individual |
| 346 single-byte characters, keysyms can be embedded by |
| 347 preceding them with "\\(" and suffixing them with ")", e.g. |
| 348 "first line\\(Return)second line" |
| 349 |
| 350 Raises: |
| 351 InputError: text string contained invalid input |
| 352 InvalidKeySymError, RuntimeError: see __get_keycode_for_keysym() |
| 353 """ |
| 354 shift_keycode = self.__get_keycode_for_keysym('Shift_L') |
| 355 shift_pressed = False |
| 356 |
| 357 i = 0 |
| 358 while i < len(text): |
| 359 ch = text[i:i+1] |
| 360 keysym = None |
| 361 if ch == '\\': |
| 362 (keysym, num_chars_to_skip) = \ |
| 363 self.__convert_escaped_string_to_keysym(text[i:]) |
| 364 i += num_chars_to_skip |
| 365 else: |
| 366 keysym = self.__convert_char_to_keysym(ch) |
| 367 i += 1 |
| 368 |
| 369 keycode = self.__get_keycode_for_keysym(keysym) |
| 370 |
| 371 # Press or release the shift key as needed for this keysym. |
| 372 shift_required = self.__keysym_requires_shift(keysym) |
| 373 if shift_required and not shift_pressed: |
| 374 xtest.fake_input( |
| 375 self.__display, X.KeyPress, detail=shift_keycode) |
| 376 shift_pressed = True |
| 377 elif not shift_required and shift_pressed: |
| 378 xtest.fake_input( |
| 379 self.__display, X.KeyRelease, detail=shift_keycode) |
| 380 shift_pressed = False |
| 381 |
| 382 xtest.fake_input(self.__display, X.KeyPress, detail=keycode) |
| 383 xtest.fake_input(self.__display, X.KeyRelease, detail=keycode) |
| 384 |
| 385 if shift_pressed: |
| 386 xtest.fake_input( |
| 387 self.__display, X.KeyRelease, detail=shift_keycode) |
| 388 self.__display.sync() |
| OLD | NEW |