Source code for utool._internal.win32_send_keys

# -*- coding: utf-8 -*-
"""
Check that SendInput can work the way we want it to

The tips and tricks at http://www.pinvoke.net/default.aspx/user32.sendinput
is useful!

"""
from __future__ import absolute_import, division, print_function, unicode_literals
import time
import ctypes

__all__ = ['KeySequenceError', 'SendKeys']

# pylint: disable-msg=R0903

DEBUG = 0

try:
    MapVirtualKey = ctypes.windll.user32.MapVirtualKeyW
    SendInput = ctypes.windll.user32.SendInput
    VkKeyScan = ctypes.windll.user32.VkKeyScanW
    VkKeyScan.restype = ctypes.c_short
    VkKeyScan.argtypes = [ctypes.c_wchar]
except AttributeError:

    def passthrough(*args, **kwargs):
        return args

    MapVirtualKey = passthrough
    SendInput = passthrough
    VkKeyScan = passthrough

DWORD = ctypes.c_ulong
LONG = ctypes.c_long
WORD = ctypes.c_ushort


# C:/PROGRA~1/MICROS~4/VC98/Include/winuser.h 4283
class MOUSEINPUT(ctypes.Structure):
    'Needed for complete definition of INPUT structure - not used'
    _pack_ = 2
    _fields_ = [
        # C:/PROGRA~1/MICROS~4/VC98/Include/winuser.h 4283
        ('dx', LONG),
        ('dy', LONG),
        ('mouseData', DWORD),
        ('dwFlags', DWORD),
        ('time', DWORD),
        ('dwExtraInfo', DWORD),
    ]


assert ctypes.sizeof(MOUSEINPUT) in [24, 48], ctypes.sizeof(MOUSEINPUT)
assert ctypes.alignment(MOUSEINPUT) == 2, ctypes.alignment(MOUSEINPUT)


# C:/PROGRA~1/MICROS~4/VC98/Include/winuser.h 4292
class KEYBDINPUT(ctypes.Structure):
    'A particular keyboard event'
    _pack_ = 2
    _fields_ = [
        # C:/PROGRA~1/MICROS~4/VC98/Include/winuser.h 4292
        ('wVk', WORD),
        ('wScan', WORD),
        ('dwFlags', DWORD),
        ('time', DWORD),
        ('dwExtraInfo', DWORD),
    ]


assert ctypes.sizeof(KEYBDINPUT) in [16, 28], ctypes.sizeof(KEYBDINPUT)
assert ctypes.alignment(KEYBDINPUT) == 2, ctypes.alignment(KEYBDINPUT)


class HARDWAREINPUT(ctypes.Structure):
    'Needed for complete definition of INPUT structure - not used'
    _pack_ = 2
    _fields_ = [
        # C:/PROGRA~1/MICROS~4/VC98/Include/winuser.h 4300
        ('uMsg', DWORD),
        ('wParamL', WORD),
        ('wParamH', WORD),
    ]


assert ctypes.sizeof(HARDWAREINPUT) in [8, 12], ctypes.sizeof(HARDWAREINPUT)
assert ctypes.alignment(HARDWAREINPUT) == 2, ctypes.alignment(HARDWAREINPUT)


# C:/PROGRA~1/MICROS~4/VC98/Include/winuser.h 4314
class UNION_INPUT_STRUCTS(ctypes.Union):
    'The C Union type representing a single Event of any type'
    _fields_ = [
        # C:/PROGRA~1/MICROS~4/VC98/Include/winuser.h 4314
        ('mi', MOUSEINPUT),
        ('ki', KEYBDINPUT),
        ('hi', HARDWAREINPUT),
    ]


assert ctypes.sizeof(UNION_INPUT_STRUCTS) in [24, 48], ctypes.sizeof(UNION_INPUT_STRUCTS)
assert ctypes.alignment(UNION_INPUT_STRUCTS) == 2, ctypes.alignment(UNION_INPUT_STRUCTS)


# C:/PROGRA~1/MICROS~4/VC98/Include/winuser.h 4310
class INPUT(ctypes.Structure):
    'See: http://msdn.microsoft.com/en-us/library/ms646270%28VS.85%29.aspx'
    _pack_ = 2
    _fields_ = [
        # C:/PROGRA~1/MICROS~4/VC98/Include/winuser.h 4310
        ('type', DWORD),
        # Unnamed field renamed to '_'
        ('_', UNION_INPUT_STRUCTS),
    ]


assert ctypes.sizeof(INPUT) in [28, 56], ctypes.sizeof(INPUT)
assert ctypes.alignment(INPUT) == 2, ctypes.alignment(INPUT)


INPUT_KEYBOARD = 1
KEYEVENTF_EXTENDEDKEY = 1
KEYEVENTF_KEYUP = 2
KEYEVENTF_UNICODE = 4
KEYEVENTF_SCANCODE = 8
VK_SHIFT = 16
VK_CONTROL = 17
VK_MENU = 18

# 'codes' recognized as {CODE( repeat)?}
CODES = {
    'BACK': 8,
    'BACKSPACE': 8,
    'BKSP': 8,
    'BREAK': 3,
    'BS': 8,
    'CAP': 20,
    'CAPSLOCK': 20,
    'DEL': 46,
    'DELETE': 46,
    'DOWN': 40,
    'END': 35,
    'ENTER': 13,
    'ESC': 27,
    'F1': 112,
    'F2': 113,
    'F3': 114,
    'F4': 115,
    'F5': 116,
    'F6': 117,
    'F7': 118,
    'F8': 119,
    'F9': 120,
    'F10': 121,
    'F11': 122,
    'F12': 123,
    'F13': 124,
    'F14': 125,
    'F15': 126,
    'F16': 127,
    'F17': 128,
    'F18': 129,
    'F19': 130,
    'F20': 131,
    'F21': 132,
    'F22': 133,
    'F23': 134,
    'F24': 135,
    'HELP': 47,
    'HOME': 36,
    'INS': 45,
    'INSERT': 45,
    'LEFT': 37,
    'LWIN': 91,
    'NUMLOCK': 144,
    'PGDN': 34,
    'PGUP': 33,
    'PRTSC': 44,
    'RIGHT': 39,
    'RMENU': 165,
    'RWIN': 92,
    'SCROLLLOCK': 145,
    'SPACE': 32,
    'TAB': 9,
    'UP': 38,
    'VK_ACCEPT': 30,
    'VK_ADD': 107,
    'VK_APPS': 93,
    'VK_ATTN': 246,
    'VK_BACK': 8,
    'VK_CANCEL': 3,
    'VK_CAPITAL': 20,
    'VK_CLEAR': 12,
    'VK_CONTROL': 17,
    'VK_CONVERT': 28,
    'VK_CRSEL': 247,
    'VK_DECIMAL': 110,
    'VK_DELETE': 46,
    'VK_DIVIDE': 111,
    'VK_DOWN': 40,
    'VK_END': 35,
    'VK_EREOF': 249,
    'VK_ESCAPE': 27,
    'VK_EXECUTE': 43,
    'VK_EXSEL': 248,
    'VK_F1': 112,
    'VK_F2': 113,
    'VK_F3': 114,
    'VK_F4': 115,
    'VK_F5': 116,
    'VK_F6': 117,
    'VK_F7': 118,
    'VK_F8': 119,
    'VK_F9': 120,
    'VK_F10': 121,
    'VK_F11': 122,
    'VK_F12': 123,
    'VK_F13': 124,
    'VK_F14': 125,
    'VK_F15': 126,
    'VK_F16': 127,
    'VK_F17': 128,
    'VK_F18': 129,
    'VK_F19': 130,
    'VK_F20': 131,
    'VK_F21': 132,
    'VK_F22': 133,
    'VK_F23': 134,
    'VK_F24': 135,
    'VK_FINAL': 24,
    'VK_HANGEUL': 21,
    'VK_HANGUL': 21,
    'VK_HANJA': 25,
    'VK_HELP': 47,
    'VK_HOME': 36,
    'VK_INSERT': 45,
    'VK_JUNJA': 23,
    'VK_KANA': 21,
    'VK_KANJI': 25,
    'VK_LBUTTON': 1,
    'VK_LCONTROL': 162,
    'VK_LEFT': 37,
    'VK_LMENU': 164,
    'VK_LSHIFT': 160,
    'VK_LWIN': 91,
    'VK_MBUTTON': 4,
    'VK_MENU': 18,
    'VK_MODECHANGE': 31,
    'VK_MULTIPLY': 106,
    'VK_NEXT': 34,
    'VK_NONAME': 252,
    'VK_NONCONVERT': 29,
    'VK_NUMLOCK': 144,
    'VK_NUMPAD0': 96,
    'VK_NUMPAD1': 97,
    'VK_NUMPAD2': 98,
    'VK_NUMPAD3': 99,
    'VK_NUMPAD4': 100,
    'VK_NUMPAD5': 101,
    'VK_NUMPAD6': 102,
    'VK_NUMPAD7': 103,
    'VK_NUMPAD8': 104,
    'VK_NUMPAD9': 105,
    'VK_OEM_CLEAR': 254,
    'VK_PA1': 253,
    'VK_PAUSE': 19,
    'VK_PLAY': 250,
    'VK_PRINT': 42,
    'VK_PRIOR': 33,
    'VK_PROCESSKEY': 229,
    'VK_RBUTTON': 2,
    'VK_RCONTROL': 163,
    'VK_RETURN': 13,
    'VK_RIGHT': 39,
    'VK_RMENU': 165,
    'VK_RSHIFT': 161,
    'VK_RWIN': 92,
    'VK_SCROLL': 145,
    'VK_SELECT': 41,
    'VK_SEPARATOR': 108,
    'VK_SHIFT': 16,
    'VK_SNAPSHOT': 44,
    'VK_SPACE': 32,
    'VK_SUBTRACT': 109,
    'VK_TAB': 9,
    'VK_UP': 38,
    'ZOOM': 251,
}
# reverse the CODES dict to make it easy to look up a particular code name
CODE_NAMES = dict((entry[1], entry[0]) for entry in CODES.items())

# modifier keys
MODIFIERS = {
    '+': VK_SHIFT,
    '^': VK_CONTROL,
    '%': VK_MENU,
}


[docs]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): self.key = key if isinstance(self.key, basestring): # NOQA self.key = unicode(key) # NOQA self.down = down self.up = up def _get_key_info(self): """Return virtual_key, scan_code, and flags for the action This is one of the methods that will be overridden by sub classes""" return 0, ord(self.key), KEYEVENTF_UNICODE def GetInput(self): 'Build the INPUT structure for the action' actions = 1 # if both up and down if self.up and self.down: actions = 2 inputs = (INPUT * actions)() vk, scan, flags = self._get_key_info() for inp in inputs: inp.type = INPUT_KEYBOARD inp._.ki.wVk = vk inp._.ki.wScan = scan inp._.ki.dwFlags |= flags # if we are releasing - then let it up if self.up: inputs[-1]._.ki.dwFlags |= KEYEVENTF_KEYUP return inputs def Run(self): 'Execute the action' inputs = self.GetInput() return SendInput(len(inputs), ctypes.byref(inputs), ctypes.sizeof(INPUT)) 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) """ down_up = '' if not (self.down and self.up): if self.down: down_up = 'down' elif self.up: down_up = 'up' return down_up def key_description(self): 'Return a description of the key' vk, scan, flags = self._get_key_info() desc = '' if vk: if vk in CODE_NAMES: desc = CODE_NAMES[vk] else: desc = 'VK %d' % vk else: desc = '%s' % self.key return desc def __str__(self): parts = [] parts.append(self.key_description()) up_down = self._get_down_up_string() if up_down: parts.append(up_down) return '<%s>' % (' '.join(parts)) __repr__ = __str__ class VirtualKeyAction(KeyAction): """Represents a virtual key action e.g. F9 DOWN, etc Overrides necessary methods of KeyAction""" def _get_key_info(self): 'Virtual keys have extended flag set' # copied more or less verbatim from # http://www.pinvoke.net/default.aspx/user32.sendinput if (self.key >= 33 and self.key <= 46) or (self.key >= 91 and self.key <= 93): flags = KEYEVENTF_EXTENDEDKEY else: flags = 0 # This works for %{F4} - ALT + F4 # return self.key, 0, 0 # this works for Tic Tac Toe i.e. +{RIGHT} SHIFT + RIGHT return self.key, MapVirtualKey(self.key, 0), flags class EscapedKeyAction(KeyAction): """Represents an escaped key action e.g. F9 DOWN, etc Overrides necessary methods of KeyAction""" def _get_key_info(self): """EscapedKeyAction doesn't send it as Unicode and the vk and scan code are generated differently""" vkey_scan = LoByte(VkKeyScan(self.key)) return (vkey_scan, MapVirtualKey(vkey_scan, 0), 0) def key_description(self): 'Return a description of the key' return 'KEsc %s' % self.key 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 '<PAUSE %1.2f>' % (self.how_long) __repr__ = __str__ # def GetInput(self): # print `self.key` # keys = KeyAction.GetInput(self) # # shift_state = HiByte(VkKeyScan(self.key)) # # shift_down = shift_state & 0x100 # 1st bit # ctrl_down = shift_state & 0x80 # 2nd bit # alt_down = shift_state & 0x40 # 3rd bit # # print bin(shift_state), shift_down, ctrl_down, alt_down # # print keys # keys = [k for k in keys] # # modifiers = [] # if shift_down: # keys[0:0] = VirtualKeyAction(VK_SHIFT, up = False).GetInput() # keys.append(VirtualKeyAction(VK_SHIFT, down = False).GetInput()) # if ctrl_down: # keys[0:0] = VirtualKeyAction(VK_CONTROL, up = False).GetInput() # keys.append(VirtualKeyAction(VK_CONTROL, down = False).GetInput()) # if alt_down: # keys[0:0] = VirtualKeyAction(VK_ALT, up = False).GetInput() # keys.append(VirtualKeyAction(VK_ALT, down = False).GetInput()) # # print keys # new_keys = (INPUT * len(keys)) () # # for i, k in enumerate(keys): # if hasattr(k, 'type'): # new_keys[i] = k # else: # for sub_key in k: # new_keys[i] = sub_key # # return new_keys # def handle_code(code): '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(VirtualKeyAction(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 %s' % count) code_keys.append(PauseAction(pause_time)) else: try: count = int(count) except ValueError: raise KeySequenceError('invalid repetition count %s' % 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([VirtualKeyAction(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: %s' % code) return code_keys def parse_keys( string, with_spaces=False, with_tabs=False, with_newlines=False, modifiers=None ): '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(): modifier = MODIFIERS[c] # remember that we are currently modified modifiers.append(modifier) # hold down the modifier key keys.append(VirtualKeyAction(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 nuewline if c in ('~', '\n'): keys.append(VirtualKeyAction(CODES['ENTER'])) # safest are the virtual keys - so if our key is a virtual key # use a VirtualKeyAction # if ord(c) in CODE_NAMES: # keys.append(VirtualKeyAction(ord(c))) elif modifiers: keys.append(EscapedKeyAction(c)) else: keys.append(KeyAction(c)) # as we have handled the text - release the modifiers while modifiers: if DEBUG: print('MODS-', modifiers) keys.append(VirtualKeyAction(modifiers.pop(), down=False)) # just in case there were any modifiers left pressed - release them while modifiers: keys.append(VirtualKeyAction(modifiers.pop(), down=False)) return keys def LoByte(val): 'Return the low byte of the value' return val & 0xFF def HiByte(val): 'Return the high byte of the value' return (val & 0xFF00) >> 8
[docs]def SendKeys( keys, pause=0.05, with_spaces=False, with_tabs=False, with_newlines=False, turn_off_numlock=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)
def main(): 'Send some test strings' actions = """ {LWIN} {PAUSE .25} r {PAUSE .25} Notepad.exe{ENTER} {PAUSE 1} Hello{SPACE}World! {PAUSE 1} %{F4} {PAUSE .25} n """ SendKeys(actions, pause=0.1) keys = parse_keys(actions) for k in keys: print(k) k.Run() time.sleep(0.1) test_strings = [ '\n' '(aa)some text\n', '(a)some{ }text\n', '(b)some{{}text\n', '(c)some{+}text\n', '(d)so%me{ab 4}text', '(e)so%me{LEFT 4}text', '(f)so%me{ENTER 4}text', '(g)so%me{^aa 4}text', '(h)some +(asdf)text', '(i)some %^+(asdf)text', '(j)some %^+a text+', '(k)some %^+a tex+{&}', '(l)some %^+a tex+(dsf)', '', ] for s in test_strings: print(repr(s)) keys = parse_keys(s, with_newlines=True) print(keys) for k in keys: k.Run() time.sleep(0.1) print() if __name__ == '__main__': main()