# -*- coding: utf-8 -*- # GUI Application automation and testing library # Copyright (C) 2006-2018 Mark Mc Mahon and Contributors # https://github.com/pywinauto/pywinauto/graphs/contributors # http://pywinauto.readthedocs.io/en/latest/credits.html # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of pywinauto nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Linux/Unix branch of the keyboard module It allows to send keystrokes to the active display using python-xlib library. """ from __future__ import print_function from Xlib.display import Display from Xlib import X from Xlib.ext.xtest import fake_input import Xlib.XK import time import six _display = Display() DEBUG = 0 spec_keysyms = { ' ': "space", '\t': "Tab", '\n': "Return", # for some reason this needs to be cr, not lf '\r': "Return", '\e': "Escape", '!': "exclam", '#': "numbersign", '%': "percent", '$': "dollar", '&': "ampersand", '"': "quotedbl", '\'': "apostrophe", '(': "parenleft", ')': "parenright", '*': "asterisk", '=': "equal", '+': "plus", ',': "comma", '-': "minus", '.': "period", '/': "slash", ':': "colon", ';': "semicolon", '<': "less", '>': "greater", '?': "question", '@': "at", '[': "bracketleft", ']': "bracketright", '\\': "backslash", '^': "asciicircum", '_': "underscore", '`': "grave", '{': "braceleft", '|': "bar", '}': "braceright", '~': "asciitilde" } def _to_keycode(key): """return python X11 keycode of symbol""" return _display.keysym_to_keycode(Xlib.XK.string_to_keysym(key)) INPUT_KEYBOARD = 1 KEYEVENTF_EXTENDEDKEY = 1 KEYEVENTF_KEYUP = 2 KEYEVENTF_UNICODE = 4 KEYEVENTF_SCANCODE = 8 VK_SHIFT = _to_keycode('Shift_L') VK_CONTROL = _to_keycode('Control_L') VK_MENU = _to_keycode('Menu') # 'codes' recognized as {CODE repeat)?} CODES = { 'BACK': _to_keycode('BackSpace'), 'BACKSPACE': _to_keycode('BackSpace'), 'BKSP': _to_keycode('BackSpace'), 'BREAK': _to_keycode('Break'), 'BS': _to_keycode('BackSpace'), 'CAP': _to_keycode('Caps_Lock'), 'CAPSLOCK': _to_keycode('Caps_Lock'), 'DEL': _to_keycode('Delete'), 'DELETE': _to_keycode('Delete'), 'DOWN': _to_keycode('Down'), 'END': _to_keycode('End'), 'ENTER': _to_keycode('Return'), 'ESC': _to_keycode('Escape'), 'F1': _to_keycode('F1'), 'F2': _to_keycode('F2'), 'F3': _to_keycode('F3'), 'F4': _to_keycode('F4'), 'F5': _to_keycode('F5'), 'F6': _to_keycode('F6'), 'F7': _to_keycode('F7'), 'F8': _to_keycode('F8'), 'F9': _to_keycode('F9'), 'F10': _to_keycode('F10'), 'F11': _to_keycode('F11'), 'F12': _to_keycode('F12'), 'F13': _to_keycode('F13'), 'F14': _to_keycode('F14'), 'F15': _to_keycode('F15'), 'F16': _to_keycode('F16'), 'F17': _to_keycode('F17'), 'F18': _to_keycode('F18'), 'F19': _to_keycode('F19'), 'F20': _to_keycode('F20'), 'F21': _to_keycode('F21'), 'F22': _to_keycode('F22'), 'F23': _to_keycode('F23'), 'F24': _to_keycode('F24'), 'HELP': _to_keycode('Help'), 'HOME': _to_keycode('Home'), 'INS': _to_keycode('Insert'), 'INSERT': _to_keycode('Insert'), 'LEFT': _to_keycode('Left'), 'LWIN': _to_keycode('Super_L'), 'NUMLOCK': _to_keycode('Num_Lock'), 'PGDN': _to_keycode('Page_Down'), 'PGUP': _to_keycode('Page_Up'), 'PRTSC': _to_keycode('Print'), 'RIGHT': _to_keycode('Right'), 'RMENU': _to_keycode('Alt_R'), 'RWIN': _to_keycode('Super_R'), 'SCROLLLOCK': _to_keycode('Scroll_Lock'), 'SPACE': _to_keycode('space'), 'TAB': _to_keycode('Tab'), 'UP': _to_keycode('Up'), 'VK_ACCEPT': 30, 'VK_ADD': 107, 'VK_APPS': 93, 'VK_ATTN': 246, 'VK_BACK': _to_keycode('BackSpace'), 'VK_CANCEL': _to_keycode('Break'), 'VK_CAPITAL': _to_keycode('Caps_Lock'), 'VK_CLEAR': 12, 'VK_CONTROL': _to_keycode('Control_L'), 'VK_CONVERT': 28, 'VK_CRSEL': 247, 'VK_DECIMAL': 110, 'VK_DELETE': _to_keycode('Delete'), 'VK_DIVIDE': 111, 'VK_DOWN': _to_keycode('Down'), 'VK_END': _to_keycode('End'), 'VK_EREOF': 249, 'VK_ESCAPE': _to_keycode('Escape'), 'VK_EXECUTE': 43, 'VK_EXSEL': 248, 'VK_F1': _to_keycode('F1'), 'VK_F2': _to_keycode('F2'), 'VK_F3': _to_keycode('F3'), 'VK_F4': _to_keycode('F4'), 'VK_F5': _to_keycode('F5'), 'VK_F6': _to_keycode('F6'), 'VK_F7': _to_keycode('F7'), 'VK_F8': _to_keycode('F8'), 'VK_F9': _to_keycode('F9'), 'VK_F10': _to_keycode('F10'), 'VK_F11': _to_keycode('F11'), 'VK_F12': _to_keycode('F12'), 'VK_F13': _to_keycode('F13'), 'VK_F14': _to_keycode('F14'), 'VK_F15': _to_keycode('F15'), 'VK_F16': _to_keycode('F16'), 'VK_F17': _to_keycode('F17'), 'VK_F18': _to_keycode('F18'), 'VK_F19': _to_keycode('F19'), 'VK_F20': _to_keycode('F20'), 'VK_F21': _to_keycode('F21'), 'VK_F22': _to_keycode('F22'), 'VK_F23': _to_keycode('F23'), 'VK_F24': _to_keycode('F24'), 'VK_FINAL': 24, 'VK_HANGEUL': 21, 'VK_HANGUL': 21, 'VK_HANJA': 25, 'VK_HELP': _to_keycode('Help'), 'VK_HOME': _to_keycode('Home'), 'VK_INSERT': _to_keycode('Insert'), 'VK_JUNJA': 23, 'VK_KANA': 21, 'VK_KANJI': 25, 'VK_LBUTTON': 1, 'VK_LCONTROL': _to_keycode('Control_L'), 'VK_LEFT': _to_keycode('Left'), 'VK_LMENU': _to_keycode('Alt_L'), 'VK_LSHIFT': _to_keycode('Shift_L'), 'VK_LWIN': _to_keycode('Super_L'), 'VK_MBUTTON': 4, 'VK_MENU': _to_keycode('Alt_L'), 'VK_MODECHANGE': 31, 'VK_MULTIPLY': 106, 'VK_NEXT': _to_keycode('Page_Down'), 'VK_NONAME': 252, 'VK_NONCONVERT': 29, 'VK_NUMLOCK': _to_keycode('Num_Lock'), 'VK_NUMPAD0': _to_keycode('KP_0'), 'VK_NUMPAD1': _to_keycode('KP_1'), 'VK_NUMPAD2': _to_keycode('KP_2'), 'VK_NUMPAD3': _to_keycode('KP_3'), 'VK_NUMPAD4': _to_keycode('KP_4'), 'VK_NUMPAD5': _to_keycode('KP_5'), 'VK_NUMPAD6': _to_keycode('KP_6'), 'VK_NUMPAD7': _to_keycode('KP_7'), 'VK_NUMPAD8': _to_keycode('KP_8'), 'VK_NUMPAD9': _to_keycode('KP_9'), 'VK_OEM_CLEAR': 254, 'VK_PA1': 253, 'VK_PAUSE': 19, 'VK_PLAY': 250, 'VK_PRINT': _to_keycode('Print'), 'VK_PRIOR': _to_keycode('Page_Up'), 'VK_PROCESSKEY': 229, 'VK_RBUTTON': 2, 'VK_RCONTROL': _to_keycode('Control_R'), 'VK_RETURN': _to_keycode('Return'), 'VK_RIGHT': _to_keycode('Right'), 'VK_RMENU': _to_keycode('Alt_R'), 'VK_RSHIFT': _to_keycode('Shift_R'), 'VK_RWIN': _to_keycode('Super_R'), 'VK_SCROLL': _to_keycode('Scroll_Lock'), 'VK_SELECT': 41, 'VK_SEPARATOR': 108, 'VK_SHIFT': _to_keycode('Shift_L'), 'VK_SNAPSHOT': _to_keycode('Print'), 'VK_SPACE': _to_keycode('Space'), 'VK_SUBTRACT': 109, 'VK_TAB': _to_keycode('Tab'), 'VK_UP': _to_keycode('Up'), 'ZOOM': 251, #no item in xlib } # modifier keys MODIFIERS = { '+': VK_SHIFT, '^': VK_CONTROL, '%': VK_MENU, } class KeySequenceError(Exception): """Exception raised when a key sequence string has a syntax error""" def __str__(self): return ' '.join(self.args) class KeyAction(object): """ Class that represents a single 'keyboard' action It represents either a PAUSE action (not reallly keyboard) or a keyboard action (press or release or both) of a particular key. """ def __init__(self, key, down = True, up = True): """Init a single key action params""" self.key = key self.down = down self.up = up self.ctrl = False self.alt = False self.shift = False self.is_shifted = False @staticmethod def _key_modifiers(ctrl, shift, alt, action = X.KeyPress): """Apply key modifiers""" if ctrl: fake_input(_display, action, CODES['VK_CONTROL']) if shift: fake_input(_display, action, CODES['VK_SHIFT']) if alt: fake_input(_display, action, CODES['VK_MENU']) def run(self): """Do a single keyboard action using xlib""" if isinstance(self.key, six.string_types): key = self.key self.key = Xlib.XK.string_to_keysym(self.key) if self.key == 0: self.key = Xlib.XK.string_to_keysym(spec_keysyms[key]) self.key = _display.keysym_to_keycode(self.key) if self.key == 0: raise RuntimeError('Key {} not found!'.format(self.key)) self.is_shifted = key.isupper() or key in '~!@#$%^&*()_+{}|:"<>?' elif not isinstance(self.key, six.integer_types): raise TypeError('self.key = {} is not a string or integer'.format(self.key)) self._key_modifiers(self.ctrl, (self.shift or self.is_shifted), self.alt, action = X.KeyPress) if self.down: fake_input(_display, X.KeyPress, self.key) _display.sync() if self.up: fake_input(_display, X.KeyRelease, self.key) _display.sync() self._key_modifiers(self.ctrl, (self.shift or self.is_shifted), self.alt, action = X.KeyRelease) _display.sync() def _get_down_up_string(self): """Return a string that will show whether the string is up or down return 'down' if the key is a press only return 'up' if the key is up only return '' if the key is up & down (as default) """ if self.down and self.up: return "" if self.down: return "down" if self.up: return "up" return "" # TODO: raise RuntimeError('Nor "down" or "up" action specified!') def key_description(self): """Return a description of the key""" return "{}".format(self.key) def __str__(self): """Return key with modifiers as a string""" parts = [] parts.append(self.key_description()) up_down = self._get_down_up_string() if up_down: parts.append(up_down) return "<{}>".format(" ".join(parts)) __repr__ = __str__ class PauseAction(KeyAction): """Represents a pause action""" def __init__(self, how_long): self.how_long = how_long def run(self): """Pause for the lenght of time specified""" time.sleep(self.how_long) def __str__(self): return "".format(self.how_long) __repr__ = __str__ def handle_code(code, vk_packet=True): """Handle a key or sequence of keys in braces""" code_keys = [] # it is a known code (e.g. {DOWN}, {ENTER}, etc) if code in CODES: code_keys.append(KeyAction(CODES[code])) # it is an escaped modifier e.g. {%}, {^}, {+} elif len(code) == 1: code_keys.append(KeyAction(code)) # it is a repetition or a pause {DOWN 5}, {PAUSE 1.3} elif ' ' in code: to_repeat, count = code.rsplit(None, 1) if to_repeat == "PAUSE": try: pause_time = float(count) except ValueError: raise KeySequenceError('invalid pause time {}'.format(count)) code_keys.append(PauseAction(pause_time)) else: try: count = int(count) except ValueError: raise KeySequenceError( 'invalid repetition count {}'.format(count)) # If the value in to_repeat is a VK e.g. DOWN # we need to add the code repeated if to_repeat in CODES: code_keys.extend( [KeyAction(CODES[to_repeat])] * count) # otherwise parse the keys and we get back a KeyAction else: to_repeat = parse_keys(to_repeat) if isinstance(to_repeat, list): keys = to_repeat * count else: keys = [to_repeat] * count code_keys.extend(keys) else: raise RuntimeError("Unknown code: {}".format(code)) return code_keys def parse_keys(string, with_spaces = False, with_tabs = False, with_newlines = False, modifiers = None, vk_packet=True): """Return the parsed keys""" keys = [] if not modifiers: modifiers = [] index = 0 while index < len(string): c = string[index] index += 1 # check if one of CTRL, SHIFT, ALT has been pressed if c in MODIFIERS.keys(): # remember that we are currently modified modifiers.append(c) # hold down the modifier key #keys.append(KeyAction(modifier, up = False)) if DEBUG: print("MODS+", modifiers) continue # Apply modifiers over a bunch of characters (not just one!) elif c == "(": # find the end of the bracketed text end_pos = string.find(")", index) if end_pos == -1: raise KeySequenceError('`)` not found') keys.extend( parse_keys(string[index:end_pos], modifiers = modifiers)) index = end_pos + 1 # Escape or named key elif c == "{": # We start searching from index + 1 to account for the case {}} end_pos = string.find("}", index+1) if end_pos == -1: raise KeySequenceError('`}` not found') code = string[index:end_pos] index = end_pos + 1 keys.extend(handle_code(code)) # unmatched ")" elif c == ')': raise KeySequenceError('`)` should be preceeded by `(`') # unmatched "}" elif c == '}': raise KeySequenceError('`}` should be preceeded by `{`') # so it is a normal character else: # don't output white space unless flags to output have been set if (c == ' ' and not with_spaces or c == '\t' and not with_tabs or c == '\n' and not with_newlines): continue # output newline if c in ('~', '\n'): keys.append(KeyAction(CODES["ENTER"])) elif modifiers: keys.append(KeyAction(c)) else: keys.append(KeyAction(c)) # as we have handled the text - release the modifiers while modifiers: if DEBUG: print("MODS-", modifiers) mod = modifiers.pop() if mod == '+': keys[-1].shift = True elif mod == '^': keys[-1].ctrl = True elif mod == '%': keys[-1].alt = True # just in case there were any modifiers left pressed - release them while modifiers: keys.append(KeyAction(modifiers.pop(), down = False)) return keys def send_keys(keys, pause=0.05, with_spaces=False, with_tabs=False, with_newlines=False, turn_off_numlock=True, vk_packet=True): """Parse the keys and type them""" keys = parse_keys(keys, with_spaces, with_tabs, with_newlines) for k in keys: k.run() time.sleep(pause)