pythonwindowschrome-devtools-protocol

How do I simulate keyboard events (down, up, press)?


I’m using PyChrome in Python to simulate keyboard events like keyDown, keyUp, and keyPress via Chrome DevTools Protocol (CDP). The type method works fine, but the down, up, and press methods aren't functioning as expected.

Issue:

  1. down: The key is not held down.
  2. up: The key is not being released.
  3. press: The key combination doesn’t simulate the key press correctly.
key_mapping = {
    "Backspace": {"code": "Backspace", "keyCode": 8},
    "Tab": {"code": "Tab", "keyCode": 9, "text": "\t"},
    "Enter": {"code": "Enter", "keyCode": 13, "text": "\r"},
    "Escape": {"code": "Escape", "keyCode": 27},
    "Space": {"code": "Space", "keyCode": 32, "text": " "},
    "Delete": {"code": "Delete", "keyCode": 46},
    "A": {"code": "KeyA", "keyCode": 65, "text": "a"},
    "B": {"code": "KeyB", "keyCode": 66, "text": "b"},
    "C": {"code": "KeyC", "keyCode": 67, "text": "c"},
    "D": {"code": "KeyD", "keyCode": 68, "text": "d"},
    "E": {"code": "KeyE", "keyCode": 69, "text": "e"},
    "F": {"code": "KeyF", "keyCode": 70, "text": "f"},
    "G": {"code": "KeyG", "keyCode": 71, "text": "g"},
    "H": {"code": "KeyH", "keyCode": 72, "text": "h"},
    "I": {"code": "KeyI", "keyCode": 73, "text": "i"},
    "J": {"code": "KeyJ", "keyCode": 74, "text": "j"},
    "K": {"code": "KeyK", "keyCode": 75, "text": "k"},
    "L": {"code": "KeyL", "keyCode": 76, "text": "l"},
    "M": {"code": "KeyM", "keyCode": 77, "text": "m"},
    "N": {"code": "KeyN", "keyCode": 78, "text": "n"},
    "O": {"code": "KeyO", "keyCode": 79, "text": "o"},
    "P": {"code": "KeyP", "keyCode": 80, "text": "p"},
    "Q": {"code": "KeyQ", "keyCode": 81, "text": "q"},
    "R": {"code": "KeyR", "keyCode": 82, "text": "r"},
    "S": {"code": "KeyS", "keyCode": 83, "text": "s"},
}

I have created a Keyboard class which would expose several methods like (up, press, down, type) Currently type is working as i expected. But for up, press or down is not working. I have tried to resolve them but they are just not working correctly.

class Keyboard:
    """Handles keyboard interactions on the page"""

    def __init__(self, tab: Tab):
        self._tab = tab

    def type(self, text: str):
        """Simulates typing text character by character."""
        for char in text:
            # self._tab.call_method("Input.dispatchKeyEvent", type="keyDown", text=char)
            self._dispatch_key_event("keyDown", text=char)

            # self._tab.call_method("Input.dispatchKeyEvent", type="keyUp", text=char)
            self._dispatch_key_event("keyUp", text=char)

    def down(self, key: str):
        """Holds down a specified key without releasing it."""
        key_data = self._get_key_data(key)
        if key_data:
            print(f"Holding down: {key} -> {key_data}")
            self._dispatch_key_event("keyDown", **key_data)

    def up(self, key: str):
        """Releases a held key."""
        key_data = self._get_key_data(key)
        if key_data:
            print(f"Releasing: {key} -> {key_data}")
            self._dispatch_key_event("keyUp", **key_data)

    def press(self, combination: str):
        keys = combination.split("+")

        for key in keys[:-1]:  # Everything except the last key
            key_data = self._get_key_data(key)
            self._dispatch_key_event("keyDown", **key_data)

        # Press the main key
        key_data = self._get_key_data(keys[-1])  # Last key in combination
        self._dispatch_key_event("keyDown", **key_data)
        self._dispatch_key_event("keyUp", **key_data)

        for key in reversed(keys[:-1]):
            key_data = self._get_key_data(key)
            self._dispatch_key_event("keyUp", **key_data)   

    def _dispatch_key_event(self, event_type: str, **kwargs):
        """Dispatches a key event to the tab."""
        print(f"Dispatching {event_type} with arguments: {kwargs}")
        self._tab.call_method(
            "Input.dispatchKeyEvent", type=event_type, _timeout=5, **kwargs
        )

    def _get_key_data(self, key: str):
        """Helper method to retrieve the appropriate key data from the key mapping."""
        normalized_key = self._normalize_key(key)
        key_data = key_mapping.get(normalized_key)
        if not key_data:
            print(f"Warning: Key '{key}' not found in key_mapping.")
        return key_data

    @staticmethod
    def _normalize_key(key: str) -> str:
        """Normalizes key names for compatibility with the key mapping."""
        key = key.capitalize()
        if key == "Ctrl":
            return "Control"
        elif key == "Cmd":
            return "Meta"  # For Mac "Cmd" becomes "Meta"
        elif key == "Controlormeta":
            # Automatically use 'Meta' for Mac, 'Control' for Windows/Linux
            return "Meta" if sys.platform == "darwin" else "Control"
        return key

So, if you call like this -> keyboard.press("Control+T") the expected behavior would open a new tab right. But it doesnot work.


Solution

  • After several attempts, I discovered that in order to implement a keyboard key press mechanic, you must send a cdp command in Unicode code.

    There are several cases to consider, such as shift, ctrl, and alt, which are considered modifier keys, and for modifiers, we have Unicode code. So, suppose you want to send a key press equivalent to shift+i. In that case, the modifiers would have to be sent using the cdp command.

    I have attached the code block for the keyboard method.

    class Keys(Enum):
        SHIFT = "\ue008"
        CONTROL = "\ue009"
        ALT = "\ue00a"
        META = "\ue03d"
        ENTER = "\ue007"
        BACKSPACE = "\ue003"
        TAB = "\ue004"
        ESCAPE = "\ue00c"
        DELETE = "\ue017"
        ARROW_LEFT = "\ue012"
        ARROW_UP = "\ue013"
        ARROW_RIGHT = "\ue014"
        ARROW_DOWN = "\ue015"
    
    
    modifierBit = {
        "\ue008": 8,  # SHIFT
        "\ue009": 2,  # CONTROL
        "\ue00a": 1,  # ALT
        "\ue03d": 4,  # META
    }
    
    keyDefinitions = {
        "a": {"key": "a", "code": "KeyA", "keyCode": 65, "shiftKey": "A", "location": 0},
        "b": {"key": "b", "code": "KeyB", "keyCode": 66, "shiftKey": "B", "location": 0},
        "c": {"key": "c", "code": "KeyC", "keyCode": 67, "shiftKey": "C", "location": 0},
        "d": {"key": "d", "code": "KeyD", "keyCode": 68, "shiftKey": "D", "location": 0},
        "e": {"key": "e", "code": "KeyE", "keyCode": 69, "shiftKey": "E", "location": 0},
        "f": {"key": "f", "code": "KeyF", "keyCode": 70, "shiftKey": "F", "location": 0},
        "g": {"key": "g", "code": "KeyG", "keyCode": 71, "shiftKey": "G", "location": 0},
        "h": {"key": "h", "code": "KeyH", "keyCode": 72, "shiftKey": "H", "location": 0},
        "i": {"key": "i", "code": "KeyI", "keyCode": 73, "shiftKey": "I", "location": 0},
        "j": {"key": "j", "code": "KeyJ", "keyCode": 74, "shiftKey": "J", "location": 0},
        "k": {"key": "k", "code": "KeyK", "keyCode": 75, "shiftKey": "K", "location": 0},
        "l": {"key": "l", "code": "KeyL", "keyCode": 76, "shiftKey": "L", "location": 0},
        "m": {"key": "m", "code": "KeyM", "keyCode": 77, "shiftKey": "M", "location": 0},
        "n": {"key": "n", "code": "KeyN", "keyCode": 78, "shiftKey": "N", "location": 0},
        "o": {"key": "o", "code": "KeyO", "keyCode": 79, "shiftKey": "O", "location": 0},
        "p": {"key": "p", "code": "KeyP", "keyCode": 80, "shiftKey": "P", "location": 0},
        "q": {"key": "q", "code": "KeyQ", "keyCode": 81, "shiftKey": "Q", "location": 0},
        "r": {"key": "r", "code": "KeyR", "keyCode": 82, "shiftKey": "R", "location": 0},
        "s": {"key": "s", "code": "KeyS", "keyCode": 83, "shiftKey": "S", "location": 0},
        "t": {"key": "t", "code": "KeyT", "keyCode": 84, "shiftKey": "T", "location": 0},
        "u": {"key": "u", "code": "KeyU", "keyCode": 85, "shiftKey": "U", "location": 0},
        "v": {"key": "v", "code": "KeyV", "keyCode": 86, "shiftKey": "V", "location": 0},
        "w": {"key": "w", "code": "KeyW", "keyCode": 87, "shiftKey": "W", "location": 0},
        "x": {"key": "x", "code": "KeyX", "keyCode": 88, "shiftKey": "X", "location": 0},
        "y": {"key": "y", "code": "KeyY", "keyCode": 89, "shiftKey": "Y", "location": 0},
        "z": {"key": "z", "code": "KeyZ", "keyCode": 90, "shiftKey": "Z", "location": 0},
        "1": {"key": "1", "code": "Digit1", "keyCode": 49, "shiftKey": "!", "location": 0},
        "2": {"key": "2", "code": "Digit2", "keyCode": 50, "shiftKey": "@", "location": 0},
        "3": {"key": "3", "code": "Digit3", "keyCode": 51, "shiftKey": "#", "location": 0},
        "4": {"key": "4", "code": "Digit4", "keyCode": 52, "shiftKey": "$", "location": 0},
        "5": {"key": "5", "code": "Digit5", "keyCode": 53, "shiftKey": "%", "location": 0},
        "6": {"key": "6", "code": "Digit6", "keyCode": 54, "shiftKey": "^", "location": 0},
        "7": {"key": "7", "code": "Digit7", "keyCode": 55, "shiftKey": "&", "location": 0},
        "8": {"key": "8", "code": "Digit8", "keyCode": 56, "shiftKey": "*", "location": 0},
        "9": {"key": "9", "code": "Digit9", "keyCode": 57, "shiftKey": "(", "location": 0},
        "0": {"key": "0", "code": "Digit0", "keyCode": 48, "shiftKey": ")", "location": 0},
        "-": {"key": "-", "code": "Minus", "keyCode": 189, "shiftKey": "_", "location": 0},
        "=": {"key": "=", "code": "Equal", "keyCode": 187, "shiftKey": "+", "location": 0},
        "[": {
            "key": "[",
            "code": "BracketLeft",
            "keyCode": 219,
            "shiftKey": "{",
            "location": 0,
        },
        "]": {
            "key": "]",
            "code": "BracketRight",
            "keyCode": 221,
            "shiftKey": "}",
            "location": 0,
        },
        "\\": {
            "key": "\\",
            "code": "Backslash",
            "keyCode": 220,
            "shiftKey": "|",
            "location": 0,
        },
        ";": {
            "key": ";",
            "code": "Semicolon",
            "keyCode": 186,
            "shiftKey": ":",
            "location": 0,
        },
        "'": {"key": "'", "code": "Quote", "keyCode": 222, "shiftKey": '"', "location": 0},
        ",": {"key": ",", "code": "Comma", "keyCode": 188, "shiftKey": "<", "location": 0},
        ".": {"key": ".", "code": "Period", "keyCode": 190, "shiftKey": ">", "location": 0},
        "/": {"key": "/", "code": "Slash", "keyCode": 191, "shiftKey": "?", "location": 0},
        "`": {
            "key": "`",
            "code": "Backquote",
            "keyCode": 192,
            "shiftKey": "~",
            "location": 0,
        },
        # Numpad
        "Numpad0": {"key": "0", "code": "Numpad0", "keyCode": 96, "location": 3},
        "Numpad1": {"key": "1", "code": "Numpad1", "keyCode": 97, "location": 3},
        "Numpad2": {"key": "2", "code": "Numpad2", "keyCode": 98, "location": 3},
        "Numpad3": {"key": "3", "code": "Numpad3", "keyCode": 99, "location": 3},
        "Numpad4": {"key": "4", "code": "Numpad4", "keyCode": 100, "location": 3},
        "Numpad5": {"key": "5", "code": "Numpad5", "keyCode": 101, "location": 3},
        "Numpad6": {"key": "6", "code": "Numpad6", "keyCode": 102, "location": 3},
        "Numpad7": {"key": "7", "code": "Numpad7", "keyCode": 103, "location": 3},
        "Numpad8": {"key": "8", "code": "Numpad8", "keyCode": 104, "location": 3},
        "Numpad9": {"key": "9", "code": "Numpad9", "keyCode": 105, "location": 3},
        "NumpadMultiply": {
            "key": "*",
            "code": "NumpadMultiply",
            "keyCode": 106,
            "location": 3,
        },
        "NumpadAdd": {"key": "+", "code": "NumpadAdd", "keyCode": 107, "location": 3},
        "NumpadSubtract": {
            "key": "-",
            "code": "NumpadSubtract",
            "keyCode": 109,
            "location": 3,
        },
        "NumpadDecimal": {
            "key": ".",
            "code": "NumpadDecimal",
            "keyCode": 110,
            "location": 3,
        },
        "NumpadDivide": {"key": "/", "code": "NumpadDivide", "keyCode": 111, "location": 3},
        "F1": {"key": "F1", "code": "F1", "keyCode": 112, "location": 0},
        "F2": {"key": "F2", "code": "F2", "keyCode": 113, "location": 0},
        "F3": {"key": "F3", "code": "F3", "keyCode": 114, "location": 0},
        "F4": {"key": "F4", "code": "F4", "keyCode": 115, "location": 0},
        "F5": {"key": "F5", "code": "F5", "keyCode": 116, "location": 0},
        "F6": {"key": "F6", "code": "F6", "keyCode": 117, "location": 0},
        "F7": {"key": "F7", "code": "F7", "keyCode": 118, "location": 0},
        "F8": {"key": "F8", "code": "F8", "keyCode": 119, "location": 0},
        "F9": {"key": "F9", "code": "F9", "keyCode": 120, "location": 0},
        "F10": {"key": "F10", "code": "F10", "keyCode": 121, "location": 0},
        "F11": {"key": "F11", "code": "F11", "keyCode": 122, "location": 0},
        "F12": {"key": "F12", "code": "F12", "keyCode": 123, "location": 0},
        "\ue007": {
            "key": "Enter",
            "code": "Enter",
            "keyCode": 13,
            "text": "\r",
            "location": 0,
        },
        "\ue003": {"key": "Backspace", "code": "Backspace", "keyCode": 8, "location": 0},
        "\ue004": {"key": "Tab", "code": "Tab", "keyCode": 9, "text": "\t", "location": 0},
        "\ue00c": {"key": "Escape", "code": "Escape", "keyCode": 27, "location": 0},
        "\ue017": {"key": "Delete", "code": "Delete", "keyCode": 46, "location": 0},
        "Space": {"key": " ", "code": "Space", "keyCode": 32, "text": " ", "location": 0},
        "\ue012": {"key": "ArrowLeft", "code": "ArrowLeft", "keyCode": 37, "location": 0},
        "\ue013": {"key": "ArrowUp", "code": "ArrowUp", "keyCode": 38, "location": 0},
        "\ue014": {"key": "ArrowRight", "code": "ArrowRight", "keyCode": 39, "location": 0},
        "\ue015": {"key": "ArrowDown", "code": "ArrowDown", "keyCode": 40, "location": 0},
        "\ue008": {"key": "Shift", "code": "ShiftLeft", "keyCode": 16, "location": 1},
        "\ue009": {"key": "Control", "code": "ControlLeft", "keyCode": 17, "location": 1},
        "\ue00a": {"key": "Alt", "code": "AltLeft", "keyCode": 18, "location": 1},
        "\ue03d": {
            "key": "Meta",
            "code": "MetaLeft",
            "keyCode": 91,
            "location": 1,
        },  # Windows key or Command key
        "CapsLock": {"key": "CapsLock", "code": "CapsLock", "keyCode": 20, "location": 0},
        "NumLock": {"key": "NumLock", "code": "NumLock", "keyCode": 144, "location": 0},
        "ScrollLock": {
            "key": "ScrollLock",
            "code": "ScrollLock",
            "keyCode": 145,
            "location": 0,
        },
        "ShiftRight": {"key": "Shift", "code": "ShiftRight", "keyCode": 16, "location": 2},
        "ControlRight": {
            "key": "Control",
            "code": "ControlRight",
            "keyCode": 17,
            "location": 2,
        },
        "AltRight": {"key": "Alt", "code": "AltRight", "keyCode": 18, "location": 2},
        "MetaRight": {
            "key": "Meta",
            "code": "MetaRight",
            "keyCode": 92,
            "location": 2,
        },  # Right Windows key or Command key
        "Home": {"key": "Home", "code": "Home", "keyCode": 36, "location": 0},
        "End": {"key": "End", "code": "End", "keyCode": 35, "location": 0},
        "PageUp": {"key": "PageUp", "code": "PageUp", "keyCode": 33, "location": 0},
        "PageDown": {"key": "PageDown", "code": "PageDown", "keyCode": 34, "location": 0},
        "Insert": {"key": "Insert", "code": "Insert", "keyCode": 45, "location": 0},
        "MediaPlayPause": {
            "key": "MediaPlayPause",
            "code": "MediaPlayPause",
            "keyCode": 179,
            "location": 0,
        },
        "MediaStop": {
            "key": "MediaStop",
            "code": "MediaStop",
            "keyCode": 178,
            "location": 0,
        },
        "MediaTrackNext": {
            "key": "MediaTrackNext",
            "code": "MediaTrackNext",
            "keyCode": 176,
            "location": 0,
        },
        "MediaTrackPrevious": {
            "key": "MediaTrackPrevious",
            "code": "MediaTrackPrevious",
            "keyCode": 177,
            "location": 0,
        },
        "AudioVolumeMute": {
            "key": "AudioVolumeMute",
            "code": "AudioVolumeMute",
            "keyCode": 173,
            "location": 0,
        },
        "AudioVolumeDown": {
            "key": "AudioVolumeDown",
            "code": "AudioVolumeDown",
            "keyCode": 174,
            "location": 0,
        },
        "AudioVolumeUp": {
            "key": "AudioVolumeUp",
            "code": "AudioVolumeUp",
            "keyCode": 175,
            "location": 0,
        },
        "BrowserBack": {
            "key": "BrowserBack",
            "code": "BrowserBack",
            "keyCode": 166,
            "location": 0,
        },
        "BrowserForward": {
            "key": "BrowserForward",
            "code": "BrowserForward",
            "keyCode": 167,
            "location": 0,
        },
        "BrowserRefresh": {
            "key": "BrowserRefresh",
            "code": "BrowserRefresh",
            "keyCode": 168,
            "location": 0,
        },
        "BrowserHome": {
            "key": "BrowserHome",
            "code": "BrowserHome",
            "keyCode": 172,
            "location": 0,
        },
        "F13": {"key": "F13", "code": "F13", "keyCode": 124, "location": 0},
        "F14": {"key": "F14", "code": "F14", "keyCode": 125, "location": 0},
        "F15": {"key": "F15", "code": "F15", "keyCode": 126, "location": 0},
        "F16": {"key": "F16", "code": "F16", "keyCode": 127, "location": 0},
        "F17": {"key": "F17", "code": "F17", "keyCode": 128, "location": 0},
        "F18": {"key": "F18", "code": "F18", "keyCode": 129, "location": 0},
        "F19": {"key": "F19", "code": "F19", "keyCode": 130, "location": 0},
        "F20": {"key": "F20", "code": "F20", "keyCode": 131, "location": 0},
        "NumpadEnter": {
            "key": "Enter",
            "code": "NumpadEnter",
            "keyCode": 13,
            "location": 3,
        },
        "NumpadEqual": {"key": "=", "code": "NumpadEqual", "keyCode": 187, "location": 3},
        "NumLock": {"key": "NumLock", "code": "NumLock", "keyCode": 144, "location": 3},
        "ContextMenu": {
            "key": "ContextMenu",
            "code": "ContextMenu",
            "keyCode": 93,
            "location": 0,
        },
        "Power": {"key": "Power", "code": "Power", "keyCode": 255, "location": 0},
        "Sleep": {"key": "Sleep", "code": "Sleep", "keyCode": 95, "location": 0},
        "WakeUp": {"key": "WakeUp", "code": "WakeUp", "keyCode": 255, "location": 0},
    }
    
    
    class Keyboard:
        """Enhanced keyboard implementation based on DrissionPage's logic"""
    
        def __init__(self, tab: Tab):
            self._tab = tab
            self.modifier = 0
    
        def make_input_data(self, key: str, key_up: bool = False) -> Optional[Dict]:
            """Creates input data for key events based on DrissionPage's implementation"""
            data = keyDefinitions.get(key)
            if not data:
                return None
    
            result = {
                "modifiers": self.modifier,
                "autoRepeat": False,
                "type": "keyUp" if key_up else "keyDown",
            }
    
            shift = self.modifier & 8
    
            if shift and data.get("shiftKey"):
                result["key"] = data["shiftKey"]
                result["text"] = data["shiftKey"]
            elif "key" in data:
                result["key"] = data["key"]
    
            if len(result.get("key", "")) == 1:
                result["text"] = result["key"]
    
            sys_text = "windowsVirtualKeyCode"
            if shift and data.get("shiftKeyCode"):
                result[sys_text] = data["shiftKeyCode"]
            elif "keyCode" in data:
                result[sys_text] = data["keyCode"]
    
            if "code" in data:
                result["code"] = data["code"]
    
            if "location" in data:
                result["location"] = data["location"]
                result["isKeypad"] = data["location"] == 3
            else:
                result["location"] = 0
                result["isKeypad"] = False
    
            if shift and data.get("shiftText"):
                result["text"] = data["shiftText"]
                result["unmodifiedText"] = data["shiftText"]
            elif "text" in data:
                result["text"] = data["text"]
                result["unmodifiedText"] = data["text"]
    
            if self.modifier & ~8:
                result["text"] = ""
    
            result["type"] = (
                "keyUp" if key_up else ("keyDown" if result.get("text") else "rawKeyDown")
            )
    
            return result
    
        def down(self, key: str):
            """Holds down a specified key"""
            key = getattr(Keys, key.upper(), key.lower())
            key_value = key.value if isinstance(key, Keys) else key
            if key_value in modifierBit:
                self.modifier |= modifierBit[key_value]
                return self
    
            data = self.make_input_data(key_value)
            if not data:
                raise ValueError(f"Invalid key: {key}")
    
            self._tab.call_method("Input.dispatchKeyEvent", **data)
            return self
    
        def up(self, key: str):
            """Releases a held key"""
            key = getattr(Keys, key.upper(), key.lower())
            key_value = key.value if isinstance(key, Keys) else key
    
            # Handle modifier keys
            if key_value in modifierBit:
                self.modifier &= ~modifierBit[key_value]
                return self
    
            # Handle regular keys
            data = self.make_input_data(key_value, key_up=True)
            if not data:
                raise ValueError(f"Invalid key: {key}")
    
            self._tab.call_method("Input.dispatchKeyEvent", **data)
            return self
    
        def type(self, text: str):
            """Types text character by character"""
            for char in text:
                if char == " ":
                    data = self.make_input_data("Space")
                else:
                    data = self.make_input_data(char)
    
                if data:
                    self._tab.call_method("Input.dispatchKeyEvent", **data)
                    
                    if char == " ":
                        up_data = self.make_input_data("Space", key_up=True)
                    else:
                        up_data = self.make_input_data(char, key_up=True)
                    if up_data:
                        self._tab.call_method("Input.dispatchKeyEvent", **up_data)
            return self