diff --git a/commandeditwnd.py b/commandeditwnd.py new file mode 100644 index 0000000..b788353 --- /dev/null +++ b/commandeditwnd.py @@ -0,0 +1,168 @@ +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +from ui_commandeditwnd import Ui_CommandEditDialog +from keyactioneditwnd import KeyActionEditWnd +from mouseactioneditwnd import MouseActionEditWnd +from pauseactioneditwnd import PauseActionEditWnd +import json +import keyboard + +class CommandEditWnd(QDialog): + def __init__(self, p_command, p_parent = None): + super().__init__(p_parent) + self.ui = Ui_CommandEditDialog() + self.ui.setupUi(self) + + self.ui.deleteBut.clicked.connect(self.slotDelete) + self.ui.ok.clicked.connect(self.slotOK) + self.ui.cancel.clicked.connect(self.slotCancel) + self.ui.keyBut.clicked.connect(self.slotNewKeyEdit) + self.ui.mouseBut.clicked.connect(self.slotNewMouseEdit) + self.ui.pauseBut.clicked.connect(self.slotNewPauseEdit) + self.ui.upBut.clicked.connect(self.slotActionUp) + self.ui.downBut.clicked.connect(self.slotActionDown) + self.ui.editBut.clicked.connect(self.slotActionEdit) + self.ui.actionsListWidget.doubleClicked.connect(self.slotActionEdit) + + w_otherMenu = QMenu() + w_otherMenu.addAction('Stop Another Command', self.slotStopAnotherCommand) + w_otherMenu.addAction('Execute Another Command', self.slotDoAnotherCommand) + self.ui.otherBut.setMenu(w_otherMenu) + + self.m_command = {} + if p_command != None: + self.ui.say.setText(p_command['name']) + w_actions = p_command['actions'] + for w_action in w_actions: + w_jsonAction = json.dumps(w_action) + w_item = QListWidgetItem(w_jsonAction) + w_item.setData(Qt.UserRole, w_jsonAction) + self.ui.actionsListWidget.addItem(w_item) + self.ui.asyncChk.setChecked(p_command['async']) + if p_command['repeat'] == -1: + self.ui.continueExe.setChecked(True) + elif p_command['repeat'] == 1: + self.ui.oneExe.setChecked(True) + else: + self.ui.repeatExe.setChecked(True) + self.ui.repeatCnt.setValue(p_command['repeat']) + else: + self.ui.asyncChk.setChecked(False) + self.ui.oneExe.setChecked(True) + + def addAction(self, p_action): + w_jsonAction = json.dumps(p_action) + w_item = QListWidgetItem(w_jsonAction) + w_item.setData(Qt.UserRole, w_jsonAction) + self.ui.actionsListWidget.addItem(w_item) + + def slotStopAnotherCommand(self): + text, okPressed = QInputDialog.getText(self, "Get Command Name", "Another command name:", QLineEdit.Normal, "") + if okPressed and text != '': + w_commandStopAction = {} + w_commandStopAction['name'] = 'command stop action' + w_commandStopAction['command name'] = text + self.addAction(w_commandStopAction) + + def slotDoAnotherCommand(self): + text, okPressed = QInputDialog.getText(self, "Get Command Name", "Another command name:", QLineEdit.Normal, "") + if okPressed and text != '': + w_commandDoAction = {} + w_commandDoAction['name'] = 'command execute action' + w_commandDoAction['command name'] = text + self.addAction(w_commandDoAction) + + def slotNewKeyEdit(self): + w_keyEditWnd = KeyActionEditWnd(None, self) + if w_keyEditWnd.exec() == QDialog.Accepted: + self.addAction(w_keyEditWnd.m_keyAction) + + def slotNewMouseEdit(self): + w_mouseEditWnd = MouseActionEditWnd(None, self) + if w_mouseEditWnd.exec() == QDialog.Accepted: + self.addAction(w_mouseEditWnd.m_mouseAction) + + def slotNewPauseEdit(self): + w_pauseEditWnd = PauseActionEditWnd(None, self) + if w_pauseEditWnd.exec() == QDialog.Accepted: + self.addAction(w_pauseEditWnd.m_pauseAction) + + def slotActionUp(self): + currentIndex = self.ui.actionsListWidget.currentRow() + currentItem = self.ui.actionsListWidget.takeItem(currentIndex); + self.ui.actionsListWidget.insertItem(currentIndex - 1, currentItem); + self.ui.actionsListWidget.setCurrentRow(currentIndex - 1); + + def slotActionDown(self): + currentIndex = self.ui.actionsListWidget.currentRow(); + currentItem = self.ui.actionsListWidget.takeItem(currentIndex); + self.ui.actionsListWidget.insertItem(currentIndex + 1, currentItem); + self.ui.actionsListWidget.setCurrentRow(currentIndex + 1); + + def slotActionEdit(self): + w_listItems = self.ui.actionsListWidget.selectedItems() + if not w_listItems: return + + w_action = {} + + for w_item in w_listItems: + w_jsonAction = w_item.data(Qt.UserRole) + w_action = json.loads(w_jsonAction) + break + + if w_action['name'] == 'key action': + w_keyEditWnd = KeyActionEditWnd(w_action, self) + if w_keyEditWnd.exec() == QDialog.Accepted: + w_jsonAction = json.dumps(w_keyEditWnd.m_keyAction) + elif w_action['name'] == 'mouse click action' \ + or w_action['name'] == 'mouse move action'\ + or w_action['name'] == 'mouse scroll action': + w_mouseEditWnd = MouseActionEditWnd(w_action, self) + if w_mouseEditWnd.exec() == QDialog.Accepted: + w_jsonAction = json.dumps(w_mouseEditWnd.m_mouseAction) + elif w_action['name'] == 'pause action': + w_pauseEditWnd = PauseActionEditWnd(w_action, self) + if w_pauseEditWnd.exec() == QDialog.Accepted: + w_jsonAction = json.dumps(w_pauseEditWnd.m_pauseAction) + elif w_action['name'] == 'command stop action' \ + or w_action['name'] == 'command execute action': + text, okPressed = QInputDialog.getText(self, "Get Command Name", "Another command name:", QLineEdit.Normal, + w_action['command name']) + if okPressed and text != '': + w_action['command name'] = text + w_jsonAction = json.dumps(w_action) + + w_item.setText(w_jsonAction) + w_item.setData(Qt.UserRole, w_jsonAction) + + def slotDelete(self): + w_listItems = self.ui.actionsListWidget.selectedItems() + if not w_listItems: return + for w_item in w_listItems: + self.ui.actionsListWidget.takeItem(self.ui.actionsListWidget.row(w_item)) + + def saveCommand(self): + w_actionCnt = self.ui.actionsListWidget.count() + self.m_command['name'] = self.ui.say.text() + w_actions = [] + for w_idx in range(w_actionCnt): + w_jsonAction = self.ui.actionsListWidget.item(w_idx).data(Qt.UserRole) + w_action = json.loads(w_jsonAction) + w_actions.append(w_action) + self.m_command['actions'] = w_actions + self.m_command['async'] = self.ui.asyncChk.isChecked() + if self.ui.oneExe.isChecked(): + self.m_command['repeat'] = 1 + elif self.ui.continueExe.isChecked(): + self.m_command['repeat'] = -1 + elif self.ui.repeatExe.isChecked(): + self.m_command['repeat'] = self.ui.repeatCnt.value() + + def slotOK(self): + self.saveCommand() + super().accept() + + def slotCancel(self): + super().reject() + diff --git a/commandeditwnd.ui b/commandeditwnd.ui new file mode 100644 index 0000000..d418733 --- /dev/null +++ b/commandeditwnd.ui @@ -0,0 +1,390 @@ + + + CommandEditDialog + + + + 0 + 0 + 927 + 419 + + + + + 10 + + + + Command Edit Dialog + + + + 25 + + + 20 + + + 20 + + + 15 + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + When this command excutes, do the following sequence: + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + When I say : + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + This command executes once + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + This command repeats continuously + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + This command repeats + + + true + + + + + + + 2 + + + + + + + times + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + Key Press + + + false + + + + + + + Mouse + + + false + + + + + + + Pause + + + false + + + + + + + Other + + + false + + + + + + + + + + 11 + + + + + + + + + + Up + + + false + + + + + + + Down + + + false + + + + + + + Edit + + + false + + + + + + + Delete + + + false + + + + + + + + + + + + + Allow other commands to execute while this one is running + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + 20 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 130 + 0 + + + + OK + + + false + + + + + + + + 130 + 0 + + + + Cancel + + + false + + + + + + + + + + diff --git a/keyactioneditwnd.py b/keyactioneditwnd.py new file mode 100644 index 0000000..d4e4541 --- /dev/null +++ b/keyactioneditwnd.py @@ -0,0 +1,152 @@ +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +from ui_keyactioneditwnd import Ui_KeyActionEditDialog +import json +import keyboard +import time +import threading + +class KeyActionEditWnd(QDialog): + def __init__(self, p_keyAction, p_parent = None): + super().__init__(p_parent) + self.ui = Ui_KeyActionEditDialog() + self.ui.setupUi(self) + + self.ui.ctrlBut.clicked.connect(self.slotCtrlClicked) + self.ui.altBut.clicked.connect(self.slotAltClicked) + self.ui.shiftBut.clicked.connect(self.slotShiftClicked) + self.ui.winBut.clicked.connect(self.slotWinClicked) + + self.ui.ok.clicked.connect(self.slotOK) + self.ui.cancel.clicked.connect(self.slotCancel) + + self.m_ctrlState = 0 + self.m_altState = 0 + self.m_shiftState = 0 + self.m_winState = 0 + + self.m_keyAction = {} + self.ui.press_releaseKey.setChecked(True) + + t = threading.Thread(target=self.keyInput) + t.daemon = True + t.start() + + if p_keyAction == None: + return + + w_hotKey = p_keyAction['key'] + w_keys = w_hotKey.split('+') + + for w_key in w_keys: + if w_key == 'left ctrl': + self.m_ctrlState = 1 + elif w_key == 'right ctrl': + self.m_ctrlState = 2 + elif w_key == 'left alt': + self.m_altState = 1 + elif w_key == 'right alt': + self.m_altState = 2 + elif w_key == 'left shift': + self.m_shiftState = 1 + elif w_key == 'right shift': + self.m_shiftState = 2 + elif w_key == 'left windows': + self.m_winState = 1 + elif w_key == 'right windows': + self.m_winState = 2 + else: + self.ui.keyEdit.setText(w_key) + + w_stateText = ['Ctrl', 'Left Ctrl', 'Right Ctrl'] + self.ui.ctrlBut.setText(w_stateText[self.m_ctrlState]) + w_stateText = ['Alt', 'Left Alt', 'Right Alt'] + self.ui.altBut.setText(w_stateText[self.m_altState]) + w_stateText = ['Shift', 'Left Shift', 'Right Shift'] + self.ui.shiftBut.setText(w_stateText[self.m_shiftState]) + w_stateText = ['Win', 'Left Win', 'Right Win'] + self.ui.winBut.setText(w_stateText[self.m_winState]) + + if p_keyAction['type'] == 10: + self.ui.press_releaseKey.setChecked(True) + elif p_keyAction['type'] == 0: + self.ui.releaseKey.setChecked(True) + elif p_keyAction['type'] == 1: + self.ui.pressKey.setChecked(True) + + def slotOK(self): + w_ctrlStateText = ['', 'left ctrl', 'right ctrl'] + w_altStateText = ['', 'left alt', 'right alt'] + w_shiftStateText = ['', 'left shift', 'right shift'] + w_winStateText = ['', 'left windows', 'right windows'] + + w_hotKey = '' + if self.m_ctrlState != 0: + w_hotKey = w_ctrlStateText[self.m_ctrlState] + + if self.m_altState != 0: + if w_hotKey != '': + w_hotKey = w_hotKey + '+' + w_hotKey = w_hotKey + w_altStateText[self.m_altState] + + if self.m_shiftState != 0: + if w_hotKey != '': + w_hotKey = w_hotKey + '+' + w_hotKey = w_hotKey + w_shiftStateText[self.m_shiftState] + + if self.m_winState != 0: + if w_hotKey != '': + w_hotKey = w_hotKey + '+' + w_hotKey = w_hotKey + w_winStateText[self.m_winState] + + if self.ui.keyEdit.text() != '': + if w_hotKey != '': + w_hotKey = w_hotKey + '+' + w_hotKey = w_hotKey + self.ui.keyEdit.text() + + if w_hotKey == '': + return + + self.m_keyAction = {} + self.m_keyAction['name'] = 'key action' + self.m_keyAction['key'] = w_hotKey + if self.ui.press_releaseKey.isChecked(): + self.m_keyAction['type'] = 10 + elif self.ui.pressKey.isChecked(): + self.m_keyAction['type'] = 1 + elif self.ui.releaseKey.isChecked(): + self.m_keyAction['type'] = 0 + + return super().accept() + + def slotCancel(self): + return super().reject() + + def slotCtrlClicked(self): + self.m_ctrlState = (self.m_ctrlState + 1) % 3 + w_stateText = ['Ctrl', 'Left Ctrl', 'Right Ctrl'] + self.ui.ctrlBut.setText(w_stateText[self.m_ctrlState]) + + def slotAltClicked(self): + self.m_altState = (self.m_altState + 1) % 3 + w_stateText = ['Alt', 'Left Alt', 'Right Alt'] + self.ui.altBut.setText(w_stateText[self.m_altState]) + + def slotShiftClicked(self): + self.m_shiftState = (self.m_shiftState + 1) % 3 + w_stateText = ['Shift', 'Left Shift', 'Right Shift'] + self.ui.shiftBut.setText(w_stateText[self.m_shiftState]) + + def slotWinClicked(self): + self.m_winState = (self.m_winState + 1) % 3 + w_stateText = ['Win', 'Left Win', 'Right Win'] + self.ui.winBut.setText(w_stateText[self.m_winState]) + + def keyInput(self): + try: + while True: + self.ui.keyEdit.setText(keyboard.read_hotkey(False)) + except: + pass + diff --git a/keyactioneditwnd.ui b/keyactioneditwnd.ui new file mode 100644 index 0000000..006f39d --- /dev/null +++ b/keyactioneditwnd.ui @@ -0,0 +1,177 @@ + + + KeyActionEditDialog + + + + 0 + 0 + 471 + 164 + + + + Key Action Dialog + + + + + + Key (Combination): + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + 20 + + + + + Press and Release Key(s) + + + + + + + Press Key(s) + + + + + + + Release Key(s) + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + OK + + + false + + + + + + + Cancel + + + false + + + + + + + + + + + + 50 + false + false + + + + Ctrl + + + false + + + + + + + Alt + + + false + + + + + + + Shift + + + false + + + + + + + Win + + + false + + + + + + + Qt::AlignCenter + + + + + + + + + + diff --git a/keyboard/__init__.py b/keyboard/__init__.py new file mode 100644 index 0000000..5d8f305 --- /dev/null +++ b/keyboard/__init__.py @@ -0,0 +1,1155 @@ +# -*- coding: utf-8 -*- +""" +keyboard +======== + +Take full control of your keyboard with this small Python library. Hook global events, register hotkeys, simulate key presses and much more. + +## Features + +- **Global event hook** on all keyboards (captures keys regardless of focus). +- **Listen** and **send** keyboard events. +- Works with **Windows** and **Linux** (requires sudo), with experimental **OS X** support (thanks @glitchassassin!). +- **Pure Python**, no C modules to be compiled. +- **Zero dependencies**. Trivial to install and deploy, just copy the files. +- **Python 2 and 3**. +- Complex hotkey support (e.g. `ctrl+shift+m, ctrl+space`) with controllable timeout. +- Includes **high level API** (e.g. [record](#keyboard.record) and [play](#keyboard.play), [add_abbreviation](#keyboard.add_abbreviation)). +- Maps keys as they actually are in your layout, with **full internationalization support** (e.g. `Ctrl+ç`). +- Events automatically captured in separate thread, doesn't block main program. +- Tested and documented. +- Doesn't break accented dead keys (I'm looking at you, pyHook). +- Mouse support available via project [mouse](https://github.com/boppreh/mouse) (`pip install mouse`). + +## Usage + +Install the [PyPI package](https://pypi.python.org/pypi/keyboard/): + + pip install keyboard + +or clone the repository (no installation required, source files are sufficient): + + git clone https://github.com/boppreh/keyboard + +or [download and extract the zip](https://github.com/boppreh/keyboard/archive/master.zip) into your project folder. + +Then check the [API docs below](https://github.com/boppreh/keyboard#api) to see what features are available. + + +## Example + + +```py +import keyboard + +keyboard.press_and_release('shift+s, space') + +keyboard.write('The quick brown fox jumps over the lazy dog.') + +keyboard.add_hotkey('ctrl+shift+a', print, args=('triggered', 'hotkey')) + +# Press PAGE UP then PAGE DOWN to type "foobar". +keyboard.add_hotkey('page up, page down', lambda: keyboard.write('foobar')) + +# Blocks until you press esc. +keyboard.wait('esc') + +# Record events until 'esc' is pressed. +recorded = keyboard.record(until='esc') +# Then replay back at three times the speed. +keyboard.play(recorded, speed_factor=3) + +# Type @@ then press space to replace with abbreviation. +keyboard.add_abbreviation('@@', 'my.long.email@example.com') + +# Block forever, like `while True`. +keyboard.wait() +``` + +## Known limitations: + +- Events generated under Windows don't report device id (`event.device == None`). [#21](https://github.com/boppreh/keyboard/issues/21) +- Media keys on Linux may appear nameless (scan-code only) or not at all. [#20](https://github.com/boppreh/keyboard/issues/20) +- Key suppression/blocking only available on Windows. [#22](https://github.com/boppreh/keyboard/issues/22) +- To avoid depending on X, the Linux parts reads raw device files (`/dev/input/input*`) +but this requries root. +- Other applications, such as some games, may register hooks that swallow all +key events. In this case `keyboard` will be unable to report events. +- This program makes no attempt to hide itself, so don't use it for keyloggers or online gaming bots. Be responsible. +""" +from __future__ import print_function as _print_function + +import re as _re +import itertools as _itertools +import collections as _collections +from threading import Thread as _Thread, Lock as _Lock +import time as _time +# Python2... Buggy on time changes and leap seconds, but no other good option (https://stackoverflow.com/questions/1205722/how-do-i-get-monotonic-time-durations-in-python). +_time.monotonic = getattr(_time, 'monotonic', None) or _time.time + +try: + # Python2 + long, basestring + _is_str = lambda x: isinstance(x, basestring) + _is_number = lambda x: isinstance(x, (int, long)) + import Queue as _queue + # threading.Event is a function in Python2 wrappin _Event (?!). + from threading import _Event as _UninterruptibleEvent +except NameError: + # Python3 + _is_str = lambda x: isinstance(x, str) + _is_number = lambda x: isinstance(x, int) + import queue as _queue + from threading import Event as _UninterruptibleEvent +_is_list = lambda x: isinstance(x, (list, tuple)) + +# Just a dynamic object to store attributes for the closures. +class _State(object): pass + +# The "Event" class from `threading` ignores signals when waiting and is +# impossible to interrupt with Ctrl+C. So we rewrite `wait` to wait in small, +# interruptible intervals. +class _Event(_UninterruptibleEvent): + def wait(self): + while True: + if _UninterruptibleEvent.wait(self, 0.5): + break + +import platform as _platform +if _platform.system() == 'Windows': + from. import _winkeyboard as _os_keyboard +elif _platform.system() == 'Linux': + from. import _nixkeyboard as _os_keyboard +elif _platform.system() == 'Darwin': + from. import _darwinkeyboard as _os_keyboard +else: + raise OSError("Unsupported platform '{}'".format(_platform.system())) + +from ._keyboard_event import KEY_DOWN, KEY_UP, KeyboardEvent +from ._generic import GenericListener as _GenericListener +from ._canonical_names import all_modifiers, sided_modifiers, normalize_name + +_modifier_scan_codes = set() +def is_modifier(key): + """ + Returns True if `key` is a scan code or name of a modifier key. + """ + if _is_str(key): + return key in all_modifiers + else: + if not _modifier_scan_codes: + scan_codes = (key_to_scan_codes(name, False) for name in all_modifiers) + _modifier_scan_codes.update(*scan_codes) + return key in _modifier_scan_codes + +_pressed_events_lock = _Lock() +_pressed_events = {} +_physically_pressed_keys = _pressed_events +_logically_pressed_keys = {} +class _KeyboardListener(_GenericListener): + transition_table = { + #Current state of the modifier, per `modifier_states`. + #| + #| Type of event that triggered this modifier update. + #| | + #| | Type of key that triggered this modiier update. + #| | | + #| | | Should we send a fake key press? + #| | | | + #| | | => | Accept the event? + #| | | | | + #| | | | | Next state. + #v v v v v v + ('free', KEY_UP, 'modifier'): (False, True, 'free'), + ('free', KEY_DOWN, 'modifier'): (False, False, 'pending'), + ('pending', KEY_UP, 'modifier'): (True, True, 'free'), + ('pending', KEY_DOWN, 'modifier'): (False, True, 'allowed'), + ('suppressed', KEY_UP, 'modifier'): (False, False, 'free'), + ('suppressed', KEY_DOWN, 'modifier'): (False, False, 'suppressed'), + ('allowed', KEY_UP, 'modifier'): (False, True, 'free'), + ('allowed', KEY_DOWN, 'modifier'): (False, True, 'allowed'), + + ('free', KEY_UP, 'hotkey'): (False, None, 'free'), + ('free', KEY_DOWN, 'hotkey'): (False, None, 'free'), + ('pending', KEY_UP, 'hotkey'): (False, None, 'suppressed'), + ('pending', KEY_DOWN, 'hotkey'): (False, None, 'suppressed'), + ('suppressed', KEY_UP, 'hotkey'): (False, None, 'suppressed'), + ('suppressed', KEY_DOWN, 'hotkey'): (False, None, 'suppressed'), + ('allowed', KEY_UP, 'hotkey'): (False, None, 'allowed'), + ('allowed', KEY_DOWN, 'hotkey'): (False, None, 'allowed'), + + ('free', KEY_UP, 'other'): (False, True, 'free'), + ('free', KEY_DOWN, 'other'): (False, True, 'free'), + ('pending', KEY_UP, 'other'): (True, True, 'allowed'), + ('pending', KEY_DOWN, 'other'): (True, True, 'allowed'), + # Necessary when hotkeys are removed after beign triggered, such as + # TestKeyboard.test_add_hotkey_multistep_suppress_modifier. + ('suppressed', KEY_UP, 'other'): (False, False, 'allowed'), + ('suppressed', KEY_DOWN, 'other'): (True, True, 'allowed'), + ('allowed', KEY_UP, 'other'): (False, True, 'allowed'), + ('allowed', KEY_DOWN, 'other'): (False, True, 'allowed'), + } + + def init(self): + _os_keyboard.init() + + self.active_modifiers = set() + self.blocking_hooks = [] + self.blocking_keys = _collections.defaultdict(list) + self.nonblocking_keys = _collections.defaultdict(list) + self.blocking_hotkeys = _collections.defaultdict(list) + self.nonblocking_hotkeys = _collections.defaultdict(list) + self.filtered_modifiers = _collections.Counter() + self.is_replaying = False + + # Supporting hotkey suppression is harder than it looks. See + # https://github.com/boppreh/keyboard/issues/22 + self.modifier_states = {} # "alt" -> "allowed" + + def pre_process_event(self, event): + for key_hook in self.nonblocking_keys[event.scan_code]: + key_hook(event) + + with _pressed_events_lock: + hotkey = tuple(sorted(_pressed_events)) + for callback in self.nonblocking_hotkeys[hotkey]: + callback(event) + + return event.scan_code or (event.name and event.name != 'unknown') + + def direct_callback(self, event): + """ + This function is called for every OS keyboard event and decides if the + event should be blocked or not, and passes a copy of the event to + other, non-blocking, listeners. + + There are two ways to block events: remapped keys, which translate + events by suppressing and re-emitting; and blocked hotkeys, which + suppress specific hotkeys. + """ + # Pass through all fake key events, don't even report to other handlers. + if self.is_replaying: + return True + + if not all(hook(event) for hook in self.blocking_hooks): + return False + + event_type = event.event_type + scan_code = event.scan_code + + # Update tables of currently pressed keys and modifiers. + with _pressed_events_lock: + if event_type == KEY_DOWN: + if is_modifier(scan_code): self.active_modifiers.add(scan_code) + _pressed_events[scan_code] = event + hotkey = tuple(sorted(_pressed_events)) + if event_type == KEY_UP: + self.active_modifiers.discard(scan_code) + if scan_code in _pressed_events: del _pressed_events[scan_code] + + # Mappings based on individual keys instead of hotkeys. + for key_hook in self.blocking_keys[scan_code]: + if not key_hook(event): + return False + + # Default accept. + accept = True + + if self.blocking_hotkeys: + if self.filtered_modifiers[scan_code]: + origin = 'modifier' + modifiers_to_update = set([scan_code]) + else: + modifiers_to_update = self.active_modifiers + if is_modifier(scan_code): + modifiers_to_update = modifiers_to_update | {scan_code} + callback_results = [callback(event) for callback in self.blocking_hotkeys[hotkey]] + if callback_results: + accept = all(callback_results) + origin = 'hotkey' + else: + origin = 'other' + + for key in sorted(modifiers_to_update): + transition_tuple = (self.modifier_states.get(key, 'free'), event_type, origin) + should_press, new_accept, new_state = self.transition_table[transition_tuple] + if should_press: press(key) + if new_accept is not None: accept = new_accept + self.modifier_states[key] = new_state + + if accept: + if event_type == KEY_DOWN: + _logically_pressed_keys[scan_code] = event + elif event_type == KEY_UP and scan_code in _logically_pressed_keys: + del _logically_pressed_keys[scan_code] + + # Queue for handlers that won't block the event. + self.queue.put(event) + + return accept + + def listen(self): + _os_keyboard.listen(self.direct_callback) + +_listener = _KeyboardListener() + +def key_to_scan_codes(key, error_if_missing=True): + """ + Returns a list of scan codes associated with this key (name or scan code). + """ + if _is_number(key): + return (key,) + elif _is_list(key): + return sum((key_to_scan_codes(i) for i in key), ()) + elif not _is_str(key): + raise ValueError('Unexpected key type ' + str(type(key)) + ', value (' + repr(key) + ')') + + normalized = normalize_name(key) + if normalized in sided_modifiers: + left_scan_codes = key_to_scan_codes('left ' + normalized, False) + right_scan_codes = key_to_scan_codes('right ' + normalized, False) + return left_scan_codes + tuple(c for c in right_scan_codes if c not in left_scan_codes) + + try: + # Put items in ordered dict to remove duplicates. + t = tuple(_collections.OrderedDict((scan_code, True) for scan_code, modifier in _os_keyboard.map_name(normalized))) + e = None + except (KeyError, ValueError) as exception: + t = () + e = exception + + if not t and error_if_missing: + raise ValueError('Key {} is not mapped to any known key.'.format(repr(key)), e) + else: + return t + +def parse_hotkey(hotkey): + """ + Parses a user-provided hotkey into nested tuples representing the + parsed structure, with the bottom values being lists of scan codes. + Also accepts raw scan codes, which are then wrapped in the required + number of nestings. + + Example: + + parse_hotkey("alt+shift+a, alt+b, c") + # Keys: ^~^ ^~~~^ ^ ^~^ ^ ^ + # Steps: ^~~~~~~~~~^ ^~~~^ ^ + + # ((alt_codes, shift_codes, a_codes), (alt_codes, b_codes), (c_codes,)) + """ + if _is_number(hotkey) or len(hotkey) == 1: + scan_codes = key_to_scan_codes(hotkey) + step = (scan_codes,) + steps = (step,) + return steps + elif _is_list(hotkey): + if not any(map(_is_list, hotkey)): + step = tuple(key_to_scan_codes(k) for k in hotkey) + steps = (step,) + return steps + return hotkey + + steps = [] + for step in _re.split(r',\s?', hotkey): + keys = _re.split(r'\s?\+\s?', step) + steps.append(tuple(key_to_scan_codes(key) for key in keys)) + return tuple(steps) + +def send(hotkey, do_press=True, do_release=True): + """ + Sends OS events that perform the given *hotkey* hotkey. + + - `hotkey` can be either a scan code (e.g. 57 for space), single key + (e.g. 'space') or multi-key, multi-step hotkey (e.g. 'alt+F4, enter'). + - `do_press` if true then press events are sent. Defaults to True. + - `do_release` if true then release events are sent. Defaults to True. + + send(57) + send('ctrl+alt+del') + send('alt+F4, enter') + send('shift+s') + + Note: keys are released in the opposite order they were pressed. + """ + _listener.is_replaying = True + + parsed = parse_hotkey(hotkey) + for step in parsed: + if do_press: + for scan_codes in step: + _os_keyboard.press(scan_codes[0]) + + if do_release: + for scan_codes in reversed(step): + _os_keyboard.release(scan_codes[0]) + + _listener.is_replaying = False + +# Alias. +press_and_release = send + +def press(hotkey): + """ Presses and holds down a hotkey (see `send`). """ + send(hotkey, True, False) + +def release(hotkey): + """ Releases a hotkey (see `send`). """ + send(hotkey, False, True) + +def is_pressed(hotkey): + """ + Returns True if the key is pressed. + + is_pressed(57) #-> True + is_pressed('space') #-> True + is_pressed('ctrl+space') #-> True + """ + _listener.start_if_necessary() + + if _is_number(hotkey): + # Shortcut. + with _pressed_events_lock: + return hotkey in _pressed_events + + steps = parse_hotkey(hotkey) + if len(steps) > 1: + raise ValueError("Impossible to check if multi-step hotkeys are pressed (`a+b` is ok, `a, b` isn't).") + + # Convert _pressed_events into a set + with _pressed_events_lock: + pressed_scan_codes = set(_pressed_events) + for scan_codes in steps[0]: + if not any(scan_code in pressed_scan_codes for scan_code in scan_codes): + return False + return True + +def call_later(fn, args=(), delay=0.001): + """ + Calls the provided function in a new thread after waiting some time. + Useful for giving the system some time to process an event, without blocking + the current execution flow. + """ + thread = _Thread(target=lambda: (_time.sleep(delay), fn(*args))) + thread.start() + +_hooks = {} +def hook(callback, suppress=False, on_remove=lambda: None): + """ + Installs a global listener on all available keyboards, invoking `callback` + each time a key is pressed or released. + + The event passed to the callback is of type `keyboard.KeyboardEvent`, + with the following attributes: + + - `name`: an Unicode representation of the character (e.g. "&") or + description (e.g. "space"). The name is always lower-case. + - `scan_code`: number representing the physical key, e.g. 55. + - `time`: timestamp of the time the event occurred, with as much precision + as given by the OS. + + Returns the given callback for easier development. + """ + if suppress: + _listener.start_if_necessary() + append, remove = _listener.blocking_hooks.append, _listener.blocking_hooks.remove + else: + append, remove = _listener.add_handler, _listener.remove_handler + + append(callback) + def remove_(): + del _hooks[callback] + del _hooks[remove_] + remove(callback) + on_remove() + _hooks[callback] = _hooks[remove_] = remove_ + return remove_ + +def on_press(callback, suppress=False): + """ + Invokes `callback` for every KEY_DOWN event. For details see `hook`. + """ + return hook(lambda e: e.event_type == KEY_UP or callback(e), suppress=suppress) + +def on_release(callback, suppress=False): + """ + Invokes `callback` for every KEY_UP event. For details see `hook`. + """ + return hook(lambda e: e.event_type == KEY_DOWN or callback(e), suppress=suppress) + +def hook_key(key, callback, suppress=False): + """ + Hooks key up and key down events for a single key. Returns the event handler + created. To remove a hooked key use `unhook_key(key)` or + `unhook_key(handler)`. + + Note: this function shares state with hotkeys, so `clear_all_hotkeys` + affects it aswell. + """ + _listener.start_if_necessary() + store = _listener.blocking_keys if suppress else _listener.nonblocking_keys + scan_codes = key_to_scan_codes(key) + for scan_code in scan_codes: + store[scan_code].append(callback) + + def remove_(): + del _hooks[callback] + del _hooks[key] + del _hooks[remove_] + for scan_code in scan_codes: + store[scan_code].remove(callback) + _hooks[callback] = _hooks[key] = _hooks[remove_] = remove_ + return remove_ + +def on_press_key(key, callback, suppress=False): + """ + Invokes `callback` for KEY_DOWN event related to the given key. For details see `hook`. + """ + return hook_key(key, lambda e: e.event_type == KEY_UP or callback(e), suppress=suppress) + +def on_release_key(key, callback, suppress=False): + """ + Invokes `callback` for KEY_UP event related to the given key. For details see `hook`. + """ + return hook_key(key, lambda e: e.event_type == KEY_DOWN or callback(e), suppress=suppress) + +def unhook(remove): + """ + Removes a previously added hook, either by callback or by the return value + of `hook`. + """ + _hooks[remove]() +unhook_key = unhook + +def unhook_all(): + """ + Removes all keyboard hooks in use, including hotkeys, abbreviations, word + listeners, `record`ers and `wait`s. + """ + _listener.start_if_necessary() + _listener.blocking_keys.clear() + _listener.nonblocking_keys.clear() + del _listener.blocking_hooks[:] + del _listener.handlers[:] + unhook_all_hotkeys() + +def block_key(key): + """ + Suppresses all key events of the given key, regardless of modifiers. + """ + return hook_key(key, lambda e: False, suppress=True) +unblock_key = unhook_key + +def remap_key(src, dst): + """ + Whenever the key `src` is pressed or released, regardless of modifiers, + press or release the hotkey `dst` instead. + """ + def handler(event): + if event.event_type == KEY_DOWN: + press(dst) + else: + release(dst) + return False + return hook_key(src, handler, suppress=True) +unremap_key = unhook_key + +def parse_hotkey_combinations(hotkey): + """ + Parses a user-provided hotkey. Differently from `parse_hotkey`, + instead of each step being a list of the different scan codes for each key, + each step is a list of all possible combinations of those scan codes. + """ + def combine_step(step): + # A single step may be composed of many keys, and each key can have + # multiple scan codes. To speed up hotkey matching and avoid introducing + # event delays, we list all possible combinations of scan codes for these + # keys. Hotkeys are usually small, and there are not many combinations, so + # this is not as insane as it sounds. + return (tuple(sorted(scan_codes)) for scan_codes in _itertools.product(*step)) + + return tuple(tuple(combine_step(step)) for step in parse_hotkey(hotkey)) + +def _add_hotkey_step(handler, combinations, suppress): + """ + Hooks a single-step hotkey (e.g. 'shift+a'). + """ + container = _listener.blocking_hotkeys if suppress else _listener.nonblocking_hotkeys + + # Register the scan codes of every possible combination of + # modfiier + main key. Modifiers have to be registered in + # filtered_modifiers too, so suppression and replaying can work. + for scan_codes in combinations: + for scan_code in scan_codes: + if is_modifier(scan_code): + _listener.filtered_modifiers[scan_code] += 1 + container[scan_codes].append(handler) + + def remove(): + for scan_codes in combinations: + for scan_code in scan_codes: + if is_modifier(scan_code): + _listener.filtered_modifiers[scan_code] -= 1 + container[scan_codes].remove(handler) + return remove + +_hotkeys = {} +def add_hotkey(hotkey, callback, args=(), suppress=False, timeout=1, trigger_on_release=False): + """ + Invokes a callback every time a hotkey is pressed. The hotkey must + be in the format `ctrl+shift+a, s`. This would trigger when the user holds + ctrl, shift and "a" at once, releases, and then presses "s". To represent + literal commas, pluses, and spaces, use their names ('comma', 'plus', + 'space'). + + - `args` is an optional list of arguments to passed to the callback during + each invocation. + - `suppress` defines if successful triggers should block the keys from being + sent to other programs. + - `timeout` is the amount of seconds allowed to pass between key presses. + - `trigger_on_release` if true, the callback is invoked on key release instead + of key press. + + The event handler function is returned. To remove a hotkey call + `remove_hotkey(hotkey)` or `remove_hotkey(handler)`. + before the hotkey state is reset. + + Note: hotkeys are activated when the last key is *pressed*, not released. + Note: the callback is executed in a separate thread, asynchronously. For an + example of how to use a callback synchronously, see `wait`. + + Examples: + + # Different but equivalent ways to listen for a spacebar key press. + add_hotkey(' ', print, args=['space was pressed']) + add_hotkey('space', print, args=['space was pressed']) + add_hotkey('Space', print, args=['space was pressed']) + # Here 57 represents the keyboard code for spacebar; so you will be + # pressing 'spacebar', not '57' to activate the print function. + add_hotkey(57, print, args=['space was pressed']) + + add_hotkey('ctrl+q', quit) + add_hotkey('ctrl+alt+enter, space', some_callback) + """ + if args: + callback = lambda callback=callback: callback(*args) + + _listener.start_if_necessary() + + steps = parse_hotkey_combinations(hotkey) + + event_type = KEY_UP if trigger_on_release else KEY_DOWN + if len(steps) == 1: + # Deciding when to allow a KEY_UP event is far harder than I thought, + # and any mistake will make that key "sticky". Therefore just let all + # KEY_UP events go through as long as that's not what we are listening + # for. + handler = lambda e: (event_type == KEY_DOWN and e.event_type == KEY_UP and e.scan_code in _logically_pressed_keys) or (event_type == e.event_type and callback()) + remove_step = _add_hotkey_step(handler, steps[0], suppress) + def remove_(): + remove_step() + del _hotkeys[hotkey] + del _hotkeys[remove_] + del _hotkeys[callback] + # TODO: allow multiple callbacks for each hotkey without overwriting the + # remover. + _hotkeys[hotkey] = _hotkeys[remove_] = _hotkeys[callback] = remove_ + return remove_ + + state = _State() + state.remove_catch_misses = None + state.remove_last_step = None + state.suppressed_events = [] + state.last_update = float('-inf') + + def catch_misses(event, force_fail=False): + if ( + event.event_type == event_type + and state.index + and event.scan_code not in allowed_keys_by_step[state.index] + ) or ( + timeout + and _time.monotonic() - state.last_update >= timeout + ) or force_fail: # Weird formatting to ensure short-circuit. + + state.remove_last_step() + + for event in state.suppressed_events: + if event.event_type == KEY_DOWN: + press(event.scan_code) + else: + release(event.scan_code) + del state.suppressed_events[:] + + index = 0 + set_index(0) + return True + + def set_index(new_index): + state.index = new_index + + if new_index == 0: + # This is done for performance reasons, avoiding a global key hook + # that is always on. + state.remove_catch_misses = lambda: None + elif new_index == 1: + state.remove_catch_misses() + # Must be `suppress=True` to ensure `send` has priority. + state.remove_catch_misses = hook(catch_misses, suppress=True) + + if new_index == len(steps) - 1: + def handler(event): + if event.event_type == KEY_UP: + remove() + set_index(0) + accept = event.event_type == event_type and callback() + if accept: + return catch_misses(event, force_fail=True) + else: + state.suppressed_events[:] = [event] + return False + remove = _add_hotkey_step(handler, steps[state.index], suppress) + else: + # Fix value of next_index. + def handler(event, new_index=state.index+1): + if event.event_type == KEY_UP: + remove() + set_index(new_index) + state.suppressed_events.append(event) + return False + remove = _add_hotkey_step(handler, steps[state.index], suppress) + state.remove_last_step = remove + state.last_update = _time.monotonic() + return False + set_index(0) + + allowed_keys_by_step = [ + set().union(*step) + for step in steps + ] + + def remove_(): + state.remove_catch_misses() + state.remove_last_step() + del _hotkeys[hotkey] + del _hotkeys[remove_] + del _hotkeys[callback] + # TODO: allow multiple callbacks for each hotkey without overwriting the + # remover. + _hotkeys[hotkey] = _hotkeys[remove_] = _hotkeys[callback] = remove_ + return remove_ +register_hotkey = add_hotkey + +def remove_hotkey(hotkey_or_callback): + """ + Removes a previously hooked hotkey. Must be called wtih the value returned + by `add_hotkey`. + """ + _hotkeys[hotkey_or_callback]() +unregister_hotkey = clear_hotkey = remove_hotkey + +def unhook_all_hotkeys(): + """ + Removes all keyboard hotkeys in use, including abbreviations, word listeners, + `record`ers and `wait`s. + """ + # Because of "alises" some hooks may have more than one entry, all of which + # are removed together. + _listener.blocking_hotkeys.clear() + _listener.nonblocking_hotkeys.clear() +unregister_all_hotkeys = remove_all_hotkeys = clear_all_hotkeys = unhook_all_hotkeys + +def remap_hotkey(src, dst, suppress=True, trigger_on_release=False): + """ + Whenever the hotkey `src` is pressed, suppress it and send + `dst` instead. + + Example: + + remap('alt+w', 'ctrl+up') + """ + def handler(): + active_modifiers = sorted(modifier for modifier, state in _listener.modifier_states.items() if state == 'allowed') + for modifier in active_modifiers: + release(modifier) + send(dst) + for modifier in reversed(active_modifiers): + press(modifier) + return False + return add_hotkey(src, handler, suppress=suppress, trigger_on_release=trigger_on_release) +unremap_hotkey = remove_hotkey + +def stash_state(): + """ + Builds a list of all currently pressed scan codes, releases them and returns + the list. Pairs well with `restore_state` and `restore_modifiers`. + """ + # TODO: stash caps lock / numlock /scrollock state. + with _pressed_events_lock: + state = sorted(_pressed_events) + for scan_code in state: + _os_keyboard.release(scan_code) + return state + +def restore_state(scan_codes): + """ + Given a list of scan_codes ensures these keys, and only these keys, are + pressed. Pairs well with `stash_state`, alternative to `restore_modifiers`. + """ + _listener.is_replaying = True + + with _pressed_events_lock: + current = set(_pressed_events) + target = set(scan_codes) + for scan_code in current - target: + _os_keyboard.release(scan_code) + for scan_code in target - current: + _os_keyboard.press(scan_code) + + _listener.is_replaying = False + +def restore_modifiers(scan_codes): + """ + Like `restore_state`, but only restores modifier keys. + """ + restore_state((scan_code for scan_code in scan_codes if is_modifier(scan_code))) + +def write(text, delay=0, restore_state_after=True, exact=None): + """ + Sends artificial keyboard events to the OS, simulating the typing of a given + text. Characters not available on the keyboard are typed as explicit unicode + characters using OS-specific functionality, such as alt+codepoint. + + To ensure text integrity, all currently pressed keys are released before + the text is typed, and modifiers are restored afterwards. + + - `delay` is the number of seconds to wait between keypresses, defaults to + no delay. + - `restore_state_after` can be used to restore the state of pressed keys + after the text is typed, i.e. presses the keys that were released at the + beginning. Defaults to True. + - `exact` forces typing all characters as explicit unicode (e.g. + alt+codepoint or special events). If None, uses platform-specific suggested + value. + """ + if exact is None: + exact = _platform.system() == 'Windows' + + state = stash_state() + + # Window's typing of unicode characters is quite efficient and should be preferred. + if exact: + for letter in text: + if letter in '\n\b': + send(letter) + else: + _os_keyboard.type_unicode(letter) + if delay: _time.sleep(delay) + else: + for letter in text: + try: + entries = _os_keyboard.map_name(normalize_name(letter)) + scan_code, modifiers = next(iter(entries)) + except (KeyError, ValueError): + _os_keyboard.type_unicode(letter) + continue + + for modifier in modifiers: + press(modifier) + + _os_keyboard.press(scan_code) + _os_keyboard.release(scan_code) + + for modifier in modifiers: + release(modifier) + + if delay: + _time.sleep(delay) + + if restore_state_after: + restore_modifiers(state) + +def wait(hotkey=None, suppress=False, trigger_on_release=False): + """ + Blocks the program execution until the given hotkey is pressed or, + if given no parameters, blocks forever. + """ + if hotkey: + lock = _Event() + remove = add_hotkey(hotkey, lambda: lock.set(), suppress=suppress, trigger_on_release=trigger_on_release) + lock.wait() + remove_hotkey(remove) + else: + while True: + _time.sleep(1e6) + +def get_hotkey_name(names=None): + """ + Returns a string representation of hotkey from the given key names, or + the currently pressed keys if not given. This function: + + - normalizes names; + - removes "left" and "right" prefixes; + - replaces the "+" key name with "plus" to avoid ambiguity; + - puts modifier keys first, in a standardized order; + - sort remaining keys; + - finally, joins everything with "+". + + Example: + + get_hotkey_name(['+', 'left ctrl', 'shift']) + # "ctrl+shift+plus" + """ + if names is None: + _listener.start_if_necessary() + with _pressed_events_lock: + names = [e.name for e in _pressed_events.values()] + else: + names = [normalize_name(name) for name in names] + clean_names = set(e.replace('left ', '').replace('right ', '').replace('+', 'plus') for e in names) + # https://developer.apple.com/macos/human-interface-guidelines/input-and-output/keyboard/ + # > List modifier keys in the correct order. If you use more than one modifier key in a + # > hotkey, always list them in this order: Control, Option, Shift, Command. + modifiers = ['ctrl', 'alt', 'shift', 'windows'] + sorting_key = lambda k: (modifiers.index(k) if k in modifiers else 5, str(k)) + return '+'.join(sorted(clean_names, key=sorting_key)) + +def read_event(suppress=False): + """ + Blocks until a keyboard event happens, then returns that event. + """ + queue = _queue.Queue(maxsize=1) + hooked = hook(queue.put, suppress=suppress) + while True: + event = queue.get() + unhook(hooked) + return event + +def read_key(suppress=False): + """ + Blocks until a keyboard event happens, then returns that event's name or, + if missing, its scan code. + """ + event = read_event(suppress) + return event.name or event.scan_code + +def read_hotkey(suppress=True): + """ + Similar to `read_key()`, but blocks until the user presses and releases a + hotkey (or single key), then returns a string representing the hotkey + pressed. + + Example: + + read_hotkey() + # "ctrl+shift+p" + """ + queue = _queue.Queue() + fn = lambda e: queue.put(e) or e.event_type == KEY_DOWN + hooked = hook(fn, suppress=suppress) + while True: + event = queue.get() + if event.event_type == KEY_UP: + unhook(hooked) + with _pressed_events_lock: + names = [e.name for e in _pressed_events.values()] + [event.name] + return get_hotkey_name(names) + +def get_typed_strings(events, allow_backspace=True): + """ + Given a sequence of events, tries to deduce what strings were typed. + Strings are separated when a non-textual key is pressed (such as tab or + enter). Characters are converted to uppercase according to shift and + capslock status. If `allow_backspace` is True, backspaces remove the last + character typed. + + This function is a generator, so you can pass an infinite stream of events + and convert them to strings in real time. + + Note this functions is merely an heuristic. Windows for example keeps per- + process keyboard state such as keyboard layout, and this information is not + available for our hooks. + + get_type_strings(record()) #-> ['This is what', 'I recorded', ''] + """ + backspace_name = 'delete' if _platform.system() == 'Darwin' else 'backspace' + + shift_pressed = False + capslock_pressed = False + string = '' + for event in events: + name = event.name + + # Space is the only key that we _parse_hotkey to the spelled out name + # because of legibility. Now we have to undo that. + if event.name == 'space': + name = ' ' + + if 'shift' in event.name: + shift_pressed = event.event_type == 'down' + elif event.name == 'caps lock' and event.event_type == 'down': + capslock_pressed = not capslock_pressed + elif allow_backspace and event.name == backspace_name and event.event_type == 'down': + string = string[:-1] + elif event.event_type == 'down': + if len(name) == 1: + if shift_pressed ^ capslock_pressed: + name = name.upper() + string = string + name + else: + yield string + string = '' + yield string + +_recording = None +def start_recording(recorded_events_queue=None): + """ + Starts recording all keyboard events into a global variable, or the given + queue if any. Returns the queue of events and the hooked function. + + Use `stop_recording()` or `unhook(hooked_function)` to stop. + """ + recorded_events_queue = recorded_events_queue or _queue.Queue() + global _recording + _recording = (recorded_events_queue, hook(recorded_events_queue.put)) + return _recording + +def stop_recording(): + """ + Stops the global recording of events and returns a list of the events + captured. + """ + global _recording + if not _recording: + raise ValueError('Must call "start_recording" before.') + recorded_events_queue, hooked = _recording + unhook(hooked) + return list(recorded_events_queue.queue) + +def record(until='escape', suppress=False, trigger_on_release=False): + """ + Records all keyboard events from all keyboards until the user presses the + given hotkey. Then returns the list of events recorded, of type + `keyboard.KeyboardEvent`. Pairs well with + `play(events)`. + + Note: this is a blocking function. + Note: for more details on the keyboard hook and events see `hook`. + """ + start_recording() + wait(until, suppress=suppress, trigger_on_release=trigger_on_release) + return stop_recording() + +def play(events, speed_factor=1.0): + """ + Plays a sequence of recorded events, maintaining the relative time + intervals. If speed_factor is <= 0 then the actions are replayed as fast + as the OS allows. Pairs well with `record()`. + + Note: the current keyboard state is cleared at the beginning and restored at + the end of the function. + """ + state = stash_state() + + last_time = None + for event in events: + if speed_factor > 0 and last_time is not None: + _time.sleep((event.time - last_time) / speed_factor) + last_time = event.time + + key = event.scan_code or event.name + press(key) if event.event_type == KEY_DOWN else release(key) + + restore_modifiers(state) +replay = play + +_word_listeners = {} +def add_word_listener(word, callback, triggers=['space'], match_suffix=False, timeout=2): + """ + Invokes a callback every time a sequence of characters is typed (e.g. 'pet') + and followed by a trigger key (e.g. space). Modifiers (e.g. alt, ctrl, + shift) are ignored. + + - `word` the typed text to be matched. E.g. 'pet'. + - `callback` is an argument-less function to be invoked each time the word + is typed. + - `triggers` is the list of keys that will cause a match to be checked. If + the user presses some key that is not a character (len>1) and not in + triggers, the characters so far will be discarded. By default the trigger + is only `space`. + - `match_suffix` defines if endings of words should also be checked instead + of only whole words. E.g. if true, typing 'carpet'+space will trigger the + listener for 'pet'. Defaults to false, only whole words are checked. + - `timeout` is the maximum number of seconds between typed characters before + the current word is discarded. Defaults to 2 seconds. + + Returns the event handler created. To remove a word listener use + `remove_word_listener(word)` or `remove_word_listener(handler)`. + + Note: all actions are performed on key down. Key up events are ignored. + Note: word mathes are **case sensitive**. + """ + state = _State() + state.current = '' + state.time = -1 + + def handler(event): + name = event.name + if event.event_type == KEY_UP or name in all_modifiers: return + + if timeout and event.time - state.time > timeout: + state.current = '' + state.time = event.time + + matched = state.current == word or (match_suffix and state.current.endswith(word)) + if name in triggers and matched: + callback() + state.current = '' + elif len(name) > 1: + state.current = '' + else: + state.current += name + + hooked = hook(handler) + def remove(): + hooked() + del _word_listeners[word] + del _word_listeners[handler] + del _word_listeners[remove] + _word_listeners[word] = _word_listeners[handler] = _word_listeners[remove] = remove + # TODO: allow multiple word listeners and removing them correctly. + return remove + +def remove_word_listener(word_or_handler): + """ + Removes a previously registered word listener. Accepts either the word used + during registration (exact string) or the event handler returned by the + `add_word_listener` or `add_abbreviation` functions. + """ + _word_listeners[word_or_handler]() + +def add_abbreviation(source_text, replacement_text, match_suffix=False, timeout=2): + """ + Registers a hotkey that replaces one typed text with another. For example + + add_abbreviation('tm', u'™') + + Replaces every "tm" followed by a space with a ™ symbol (and no space). The + replacement is done by sending backspace events. + + - `match_suffix` defines if endings of words should also be checked instead + of only whole words. E.g. if true, typing 'carpet'+space will trigger the + listener for 'pet'. Defaults to false, only whole words are checked. + - `timeout` is the maximum number of seconds between typed characters before + the current word is discarded. Defaults to 2 seconds. + + For more details see `add_word_listener`. + """ + replacement = '\b'*(len(source_text)+1) + replacement_text + callback = lambda: write(replacement) + return add_word_listener(source_text, callback, match_suffix=match_suffix, timeout=timeout) + +# Aliases. +register_word_listener = add_word_listener +register_abbreviation = add_abbreviation +remove_abbreviation = remove_word_listener \ No newline at end of file diff --git a/keyboard/__main__.py b/keyboard/__main__.py new file mode 100644 index 0000000..3acc6f6 --- /dev/null +++ b/keyboard/__main__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +import keyboard +import fileinput +import json +import sys + +def print_event_json(event): + print(event.to_json(ensure_ascii=sys.stdout.encoding != 'utf-8')) + sys.stdout.flush() +keyboard.hook(print_event_json) + +parse_event_json = lambda line: keyboard.KeyboardEvent(**json.loads(line)) +keyboard.play(parse_event_json(line) for line in fileinput.input()) \ No newline at end of file diff --git a/keyboard/__pycache__/__init__.cpython-36.pyc b/keyboard/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000..72aa11a Binary files /dev/null and b/keyboard/__pycache__/__init__.cpython-36.pyc differ diff --git a/keyboard/__pycache__/_canonical_names.cpython-36.pyc b/keyboard/__pycache__/_canonical_names.cpython-36.pyc new file mode 100644 index 0000000..77a2f90 Binary files /dev/null and b/keyboard/__pycache__/_canonical_names.cpython-36.pyc differ diff --git a/keyboard/__pycache__/_generic.cpython-36.pyc b/keyboard/__pycache__/_generic.cpython-36.pyc new file mode 100644 index 0000000..e07e1f9 Binary files /dev/null and b/keyboard/__pycache__/_generic.cpython-36.pyc differ diff --git a/keyboard/__pycache__/_keyboard_event.cpython-36.pyc b/keyboard/__pycache__/_keyboard_event.cpython-36.pyc new file mode 100644 index 0000000..47fb9d5 Binary files /dev/null and b/keyboard/__pycache__/_keyboard_event.cpython-36.pyc differ diff --git a/keyboard/__pycache__/_nixcommon.cpython-36.pyc b/keyboard/__pycache__/_nixcommon.cpython-36.pyc new file mode 100644 index 0000000..ca5d3bd Binary files /dev/null and b/keyboard/__pycache__/_nixcommon.cpython-36.pyc differ diff --git a/keyboard/__pycache__/_nixkeyboard.cpython-36.pyc b/keyboard/__pycache__/_nixkeyboard.cpython-36.pyc new file mode 100644 index 0000000..a0fa00b Binary files /dev/null and b/keyboard/__pycache__/_nixkeyboard.cpython-36.pyc differ diff --git a/keyboard/__pycache__/_winkeyboard.cpython-36.pyc b/keyboard/__pycache__/_winkeyboard.cpython-36.pyc new file mode 100644 index 0000000..8547f03 Binary files /dev/null and b/keyboard/__pycache__/_winkeyboard.cpython-36.pyc differ diff --git a/keyboard/_canonical_names.py b/keyboard/_canonical_names.py new file mode 100644 index 0000000..003fd3b --- /dev/null +++ b/keyboard/_canonical_names.py @@ -0,0 +1,1246 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +try: + basestring +except NameError: + basestring = str + +import platform + +# Defaults to Windows canonical names (platform-specific overrides below) +canonical_names = { + 'escape': 'esc', + 'return': 'enter', + 'del': 'delete', + 'control': 'ctrl', + + 'left arrow': 'left', + 'up arrow': 'up', + 'down arrow': 'down', + 'right arrow': 'right', + + ' ': 'space', # Prefer to spell out keys that would be hard to read. + '\x1b': 'esc', + '\x08': 'backspace', + '\n': 'enter', + '\t': 'tab', + '\r': 'enter', + + 'scrlk': 'scroll lock', + 'prtscn': 'print screen', + 'prnt scrn': 'print screen', + 'snapshot': 'print screen', + 'ins': 'insert', + 'pause break': 'pause', + 'ctrll lock': 'caps lock', + 'capslock': 'caps lock', + 'number lock': 'num lock', + 'numlock': 'num lock', + 'space bar': 'space', + 'spacebar': 'space', + 'linefeed': 'enter', + 'win': 'windows', + + # Mac keys + 'command': 'windows', + 'cmd': 'windows', + 'control': 'ctrl', + 'option': 'alt', + + 'app': 'menu', + 'apps': 'menu', + 'application': 'menu', + 'applications': 'menu', + + 'pagedown': 'page down', + 'pageup': 'page up', + 'pgdown': 'page down', + 'pgup': 'page up', + + 'play/pause': 'play/pause media', + + 'num multiply': '*', + 'num divide': '/', + 'num add': '+', + 'num plus': '+', + 'num minus': '-', + 'num sub': '-', + 'num enter': 'enter', + 'num 0': '0', + 'num 1': '1', + 'num 2': '2', + 'num 3': '3', + 'num 4': '4', + 'num 5': '5', + 'num 6': '6', + 'num 7': '7', + 'num 8': '8', + 'num 9': '9', + + 'left win': 'left windows', + 'right win': 'right windows', + 'left control': 'left ctrl', + 'right control': 'right ctrl', + 'left menu': 'left alt', # Windows... + 'altgr': 'alt gr', + + # https://www.x.org/releases/X11R7.6/doc/libX11/Compose/en_US.UTF-8.html + # https://svn.apache.org/repos/asf/xmlgraphics/commons/tags/commons-1_0/src/java/org/apache/xmlgraphics/fonts/Glyphs.java + # Note this list has plenty of uppercase letters that are not being used + # at the moment, as normalization forces names to be lowercase. + "Aacute": "Á", + "aacute": "á", + "Aacutesmall": "", + "abovedot": "˙", + "Abreve": "Ă", + "abreve": "ă", + "Abreveacute": "Ắ", + "abreveacute": "ắ", + "Abrevebelowdot": "Ặ", + "abrevebelowdot": "ặ", + "Abrevegrave": "Ằ", + "abrevegrave": "ằ", + "Abrevehook": "Ẳ", + "abrevehook": "ẳ", + "Abrevetilde": "Ẵ", + "abrevetilde": "ẵ", + "Acircumflex": "Â", + "acircumflex": "â", + "Acircumflexacute": "Ấ", + "acircumflexacute": "ấ", + "Acircumflexbelowdot": "Ậ", + "acircumflexbelowdot": "ậ", + "Acircumflexgrave": "Ầ", + "acircumflexgrave": "ầ", + "Acircumflexhook": "Ẩ", + "acircumflexhook": "ẩ", + "Acircumflexsmall": "", + "Acircumflextilde": "Ẫ", + "acircumflextilde": "ẫ", + "acute": "´", + "Acute": "", + "acutecomb": "́", + "Acutesmall": "", + "add": "+", + "Adiaeresis": "Ä", + "adiaeresis": "ä", + "Adieresis": "Ä", + "adieresis": "ä", + "Adieresissmall": "", + "ae": "æ", + "AE": "Æ", + "AEacute": "Ǽ", + "aeacute": "ǽ", + "AEsmall": "", + "afii00208": "―", + "afii10017": "А", + "afii10018": "Б", + "afii10019": "В", + "afii10020": "Г", + "afii10021": "Д", + "afii10022": "Е", + "afii10023": "Ё", + "afii10024": "Ж", + "afii10025": "З", + "afii10026": "И", + "afii10027": "Й", + "afii10028": "К", + "afii10029": "Л", + "afii10030": "М", + "afii10031": "Н", + "afii10032": "О", + "afii10033": "П", + "afii10034": "Р", + "afii10035": "С", + "afii10036": "Т", + "afii10037": "У", + "afii10038": "Ф", + "afii10039": "Х", + "afii10040": "Ц", + "afii10041": "Ч", + "afii10042": "Ш", + "afii10043": "Щ", + "afii10044": "Ъ", + "afii10045": "Ы", + "afii10046": "Ь", + "afii10047": "Э", + "afii10048": "Ю", + "afii10049": "Я", + "afii10050": "Ґ", + "afii10051": "Ђ", + "afii10052": "Ѓ", + "afii10053": "Є", + "afii10054": "Ѕ", + "afii10055": "І", + "afii10056": "Ї", + "afii10057": "Ј", + "afii10058": "Љ", + "afii10059": "Њ", + "afii10060": "Ћ", + "afii10061": "Ќ", + "afii10062": "Ў", + "afii10063": "", + "afii10064": "", + "afii10065": "а", + "afii10066": "б", + "afii10067": "в", + "afii10068": "г", + "afii10069": "д", + "afii10070": "е", + "afii10071": "ё", + "afii10072": "ж", + "afii10073": "з", + "afii10074": "и", + "afii10075": "й", + "afii10076": "к", + "afii10077": "л", + "afii10078": "м", + "afii10079": "н", + "afii10080": "о", + "afii10081": "п", + "afii10082": "р", + "afii10083": "с", + "afii10084": "т", + "afii10085": "у", + "afii10086": "ф", + "afii10087": "х", + "afii10088": "ц", + "afii10089": "ч", + "afii10090": "ш", + "afii10091": "щ", + "afii10092": "ъ", + "afii10093": "ы", + "afii10094": "ь", + "afii10095": "э", + "afii10096": "ю", + "afii10097": "я", + "afii10098": "ґ", + "afii10099": "ђ", + "afii10100": "ѓ", + "afii10101": "є", + "afii10102": "ѕ", + "afii10103": "і", + "afii10104": "ї", + "afii10105": "ј", + "afii10106": "љ", + "afii10107": "њ", + "afii10108": "ћ", + "afii10109": "ќ", + "afii10110": "ў", + "afii10145": "Џ", + "afii10146": "Ѣ", + "afii10147": "Ѳ", + "afii10148": "Ѵ", + "afii10192": "", + "afii10193": "џ", + "afii10194": "ѣ", + "afii10195": "ѳ", + "afii10196": "ѵ", + "afii10831": "", + "afii10832": "", + "afii10846": "ә", + "afii299": "‎", + "afii300": "‏", + "afii301": "‍", + "afii57381": "٪", + "afii57388": "،", + "afii57392": "٠", + "afii57393": "١", + "afii57394": "٢", + "afii57395": "٣", + "afii57396": "٤", + "afii57397": "٥", + "afii57398": "٦", + "afii57399": "٧", + "afii57400": "٨", + "afii57401": "٩", + "afii57403": "؛", + "afii57407": "؟", + "afii57409": "ء", + "afii57410": "آ", + "afii57411": "أ", + "afii57412": "ؤ", + "afii57413": "إ", + "afii57414": "ئ", + "afii57415": "ا", + "afii57416": "ب", + "afii57417": "ة", + "afii57418": "ت", + "afii57419": "ث", + "afii57420": "ج", + "afii57421": "ح", + "afii57422": "خ", + "afii57423": "د", + "afii57424": "ذ", + "afii57425": "ر", + "afii57426": "ز", + "afii57427": "س", + "afii57428": "ش", + "afii57429": "ص", + "afii57430": "ض", + "afii57431": "ط", + "afii57432": "ظ", + "afii57433": "ع", + "afii57434": "غ", + "afii57440": "ـ", + "afii57441": "ف", + "afii57442": "ق", + "afii57443": "ك", + "afii57444": "ل", + "afii57445": "م", + "afii57446": "ن", + "afii57448": "و", + "afii57449": "ى", + "afii57450": "ي", + "afii57451": "ً", + "afii57452": "ٌ", + "afii57453": "ٍ", + "afii57454": "َ", + "afii57455": "ُ", + "afii57456": "ِ", + "afii57457": "ّ", + "afii57458": "ْ", + "afii57470": "ه", + "afii57505": "ڤ", + "afii57506": "پ", + "afii57507": "چ", + "afii57508": "ژ", + "afii57509": "گ", + "afii57511": "ٹ", + "afii57512": "ڈ", + "afii57513": "ڑ", + "afii57514": "ں", + "afii57519": "ے", + "afii57534": "ە", + "afii57636": "₪", + "afii57645": "־", + "afii57658": "׃", + "afii57664": "א", + "afii57665": "ב", + "afii57666": "ג", + "afii57667": "ד", + "afii57668": "ה", + "afii57669": "ו", + "afii57670": "ז", + "afii57671": "ח", + "afii57672": "ט", + "afii57673": "י", + "afii57674": "ך", + "afii57675": "כ", + "afii57676": "ל", + "afii57677": "ם", + "afii57678": "מ", + "afii57679": "ן", + "afii57680": "נ", + "afii57681": "ס", + "afii57682": "ע", + "afii57683": "ף", + "afii57684": "פ", + "afii57685": "ץ", + "afii57686": "צ", + "afii57687": "ק", + "afii57688": "ר", + "afii57689": "ש", + "afii57690": "ת", + "afii57694": "שׁ", + "afii57695": "שׂ", + "afii57700": "וֹ", + "afii57705": "ײַ", + "afii57716": "װ", + "afii57717": "ױ", + "afii57718": "ײ", + "afii57723": "וּ", + "afii57793": "ִ", + "afii57794": "ֵ", + "afii57795": "ֶ", + "afii57796": "ֻ", + "afii57797": "ָ", + "afii57798": "ַ", + "afii57799": "ְ", + "afii57800": "ֲ", + "afii57801": "ֱ", + "afii57802": "ֳ", + "afii57803": "ׂ", + "afii57804": "ׁ", + "afii57806": "ֹ", + "afii57807": "ּ", + "afii57839": "ֽ", + "afii57841": "ֿ", + "afii57842": "׀", + "afii57929": "ʼ", + "afii61248": "℅", + "afii61289": "ℓ", + "afii61352": "№", + "afii61573": "‬", + "afii61574": "‭", + "afii61575": "‮", + "afii61664": "‌", + "afii63167": "٭", + "afii64937": "ʽ", + "Agrave": "À", + "agrave": "à", + "Agravesmall": "", + "agudo": "´", + "aleph": "ℵ", + "Alpha": "Α", + "alpha": "α", + "Alphatonos": "Ά", + "alphatonos": "ά", + "Amacron": "Ā", + "amacron": "ā", + "ampersand": "&", + "ampersandsmall": "", + "angle": "∠", + "angleleft": "〈", + "angleright": "〉", + "anoteleia": "·", + "Aogonek": "Ą", + "aogonek": "ą", + "apostrophe": "'", + "approxequal": "≈", + "Aring": "Å", + "aring": "å", + "Aringacute": "Ǻ", + "aringacute": "ǻ", + "Aringsmall": "", + "arrowboth": "↔", + "arrowdblboth": "⇔", + "arrowdbldown": "⇓", + "arrowdblleft": "⇐", + "arrowdblright": "⇒", + "arrowdblup": "⇑", + "arrowdown": "↓", + "arrowhorizex": "", + "arrowleft": "←", + "arrowright": "→", + "arrowup": "↑", + "arrowupdn": "↕", + "arrowupdnbse": "↨", + "arrowvertex": "", + "asciicircum": "^", + "asciitilde": "~", + "Asmall": "", + "asterisk": "*", + "asteriskmath": "∗", + "asuperior": "", + "at": "@", + "Atilde": "Ã", + "atilde": "ã", + "Atildesmall": "", + "backslash": "\\", + "bar": "|", + "Beta": "Β", + "beta": "β", + "block": "█", + "braceex": "", + "braceleft": "{", + "braceleftbt": "", + "braceleftmid": "", + "bracelefttp": "", + "braceright": "}", + "bracerightbt": "", + "bracerightmid": "", + "bracerighttp": "", + "bracketleft": "[", + "bracketleftbt": "", + "bracketleftex": "", + "bracketlefttp": "", + "bracketright": "]", + "bracketrightbt": "", + "bracketrightex": "", + "bracketrighttp": "", + "breve": "˘", + "Brevesmall": "", + "brokenbar": "¦", + "Bsmall": "", + "bsuperior": "", + "bullet": "•", + "Cacute": "Ć", + "cacute": "ć", + "caron": "ˇ", + "Caron": "", + "Caronsmall": "", + "carriagereturn": "↵", + "Ccaron": "Č", + "ccaron": "č", + "Ccedilla": "Ç", + "ccedilla": "ç", + "Ccedillasmall": "", + "Ccircumflex": "Ĉ", + "ccircumflex": "ĉ", + "Cdotaccent": "Ċ", + "cdotaccent": "ċ", + "cedilla": "¸", + "Cedillasmall": "", + "cent": "¢", + "centinferior": "", + "centoldstyle": "", + "centsuperior": "", + "Chi": "Χ", + "chi": "χ", + "circle": "○", + "circlemultiply": "⊗", + "circleplus": "⊕", + "circumflex": "^", + "circumflex": "ˆ", + "Circumflexsmall": "", + "club": "♣", + "colon": ":", + "colonmonetary": "₡", + "ColonSign": "₡", + "comma": ",", + "commaaccent": "", + "commainferior": "", + "commasuperior": "", + "congruent": "≅", + "copyright": "©", + "copyrightsans": "", + "copyrightserif": "", + "CruzeiroSign": "₢", + "Csmall": "", + "currency": "¤", + "cyrBreve": "", + "cyrbreve": "", + "cyrFlex": "", + "cyrflex": "", + "dagger": "†", + "daggerdbl": "‡", + "dblGrave": "", + "dblgrave": "", + "Dcaron": "Ď", + "dcaron": "ď", + "Dcroat": "Đ", + "dcroat": "đ", + "degree": "°", + "Delta": "Δ", + "delta": "δ", + "diaeresis": "¨", + "diamond": "♦", + "dieresis": "¨", + "Dieresis": "", + "DieresisAcute": "", + "dieresisacute": "", + "DieresisGrave": "", + "dieresisgrave": "", + "Dieresissmall": "", + "dieresistonos": "΅", + "divide": "/", + "divide": "÷", + "division": "÷", + "dkshade": "▓", + "dnblock": "▄", + "dollar": "$", + "dollarinferior": "", + "dollaroldstyle": "", + "dollarsuperior": "", + "dong": "₫", + "DongSign": "₫", + "dot": ".", + "dotaccent": "˙", + "Dotaccentsmall": "", + "dotbelowcomb": "̣", + "dotlessi": "ı", + "dotlessj": "", + "dotmath": "⋅", + "Dsmall": "", + "dstroke": "đ", + "Dstroke": "Đ", + "dsuperior": "", + "Eacute": "É", + "eacute": "é", + "Eacutesmall": "", + "Ebreve": "Ĕ", + "ebreve": "ĕ", + "Ecaron": "Ě", + "ecaron": "ě", + "Ecircumflex": "Ê", + "ecircumflex": "ê", + "Ecircumflexacute": "Ế", + "ecircumflexacute": "ế", + "Ecircumflexbelowdot": "Ệ", + "ecircumflexbelowdot": "ệ", + "Ecircumflexgrave": "Ề", + "ecircumflexgrave": "ề", + "Ecircumflexhook": "Ể", + "ecircumflexhook": "ể", + "Ecircumflexsmall": "", + "Ecircumflextilde": "Ễ", + "ecircumflextilde": "ễ", + "EcuSign": "₠", + "Ediaeresis": "Ë", + "ediaeresis": "ë", + "Edieresis": "Ë", + "edieresis": "ë", + "Edieresissmall": "", + "Edotaccent": "Ė", + "edotaccent": "ė", + "Egrave": "È", + "egrave": "è", + "Egravesmall": "", + "eight": "8", + "eightinferior": "₈", + "eightoldstyle": "", + "eightsubscript": "₈", + "eightsuperior": "⁸", + "element": "∈", + "ellipsis": "…", + "Emacron": "Ē", + "emacron": "ē", + "emdash": "—", + "emptyset": "∅", + "endash": "–", + "enfilledcircbullet": "•", + "Eng": "Ŋ", + "eng": "ŋ", + "Eogonek": "Ę", + "eogonek": "ę", + "Epsilon": "Ε", + "epsilon": "ε", + "Epsilontonos": "Έ", + "epsilontonos": "έ", + "equal": "=", + "equivalence": "≡", + "Esmall": "", + "estimated": "℮", + "esuperior": "", + "Eta": "Η", + "eta": "η", + "Etatonos": "Ή", + "etatonos": "ή", + "ETH": "Ð", + "eth": "ð", + "Eth": "Ð", + "Ethsmall": "", + "euro": "€", + "Euro": "€", + "EuroSign": "€", + "exclam": "!", + "exclamdbl": "‼", + "exclamdown": "¡", + "exclamdownsmall": "", + "exclamsmall": "", + "existential": "∃", + "female": "♀", + "ff": "ff", + "ffi": "ffi", + "ffl": "ffl", + "FFrancSign": "₣", + "fi": "fi", + "figuredash": "‒", + "filledbox": "■", + "filledrect": "▬", + "five": "5", + "fiveeighths": "⅝", + "fiveinferior": "₅", + "fiveoldstyle": "", + "fivesubscript": "₅", + "fivesuperior": "⁵", + "fl": "fl", + "florin": "ƒ", + "four": "4", + "fourinferior": "₄", + "fouroldstyle": "", + "foursubscript": "₄", + "foursuperior": "⁴", + "fraction": "∕", + "franc": "₣", + "Fsmall": "", + "function": "ƒ", + "Gamma": "Γ", + "gamma": "γ", + "Gbreve": "Ğ", + "gbreve": "ğ", + "Gcaron": "Ǧ", + "gcaron": "ǧ", + "Gcircumflex": "Ĝ", + "gcircumflex": "ĝ", + "Gcommaaccent": "Ģ", + "gcommaaccent": "ģ", + "Gdotaccent": "Ġ", + "gdotaccent": "ġ", + "germandbls": "ß", + "gradient": "∇", + "grave": "`", + "Grave": "", + "gravecomb": "̀", + "Gravesmall": "", + "greater": ">", + "greaterequal": "≥", + "Gsmall": "", + "guillemotleft": "«", + "guillemotright": "»", + "guilsinglleft": "‹", + "guilsinglright": "›", + "H18533": "●", + "H18543": "▪", + "H18551": "▫", + "H22073": "□", + "hash": "#", + "hashtag": "#", + "Hbar": "Ħ", + "hbar": "ħ", + "Hcircumflex": "Ĥ", + "hcircumflex": "ĥ", + "heart": "♥", + "hookabovecomb": "̉", + "house": "⌂", + "Hsmall": "", + "hungarumlaut": "˝", + "Hungarumlaut": "", + "Hungarumlautsmall": "", + "hyphen": "­", + "hypheninferior": "", + "hyphensuperior": "", + "Iacute": "Í", + "iacute": "í", + "Iacutesmall": "", + "Ibreve": "Ĭ", + "ibreve": "ĭ", + "Icircumflex": "Î", + "icircumflex": "î", + "Icircumflexsmall": "", + "Idiaeresis": "Ï", + "idiaeresis": "ï", + "Idieresis": "Ï", + "idieresis": "ï", + "Idieresissmall": "", + "Idotaccent": "İ", + "Ifraktur": "ℑ", + "Igrave": "Ì", + "igrave": "ì", + "Igravesmall": "", + "IJ": "IJ", + "ij": "ij", + "Imacron": "Ī", + "imacron": "ī", + "infinity": "∞", + "integral": "∫", + "integralbt": "⌡", + "integralex": "", + "integraltp": "⌠", + "intersection": "∩", + "invbullet": "◘", + "invcircle": "◙", + "invsmileface": "☻", + "Iogonek": "Į", + "iogonek": "į", + "Iota": "Ι", + "iota": "ι", + "Iotadieresis": "Ϊ", + "iotadieresis": "ϊ", + "iotadieresistonos": "ΐ", + "Iotatonos": "Ί", + "iotatonos": "ί", + "Ismall": "", + "isuperior": "", + "Itilde": "Ĩ", + "itilde": "ĩ", + "Jcircumflex": "Ĵ", + "jcircumflex": "ĵ", + "Jsmall": "", + "Kappa": "Κ", + "kappa": "κ", + "Kcommaaccent": "Ķ", + "kcommaaccent": "ķ", + "kgreenlandic": "ĸ", + "Ksmall": "", + "Lacute": "Ĺ", + "lacute": "ĺ", + "Lambda": "Λ", + "lambda": "λ", + "Lcaron": "Ľ", + "lcaron": "ľ", + "Lcommaaccent": "Ļ", + "lcommaaccent": "ļ", + "Ldot": "Ŀ", + "ldot": "ŀ", + "less": "<", + "lessequal": "≤", + "lfblock": "▌", + "lira": "₤", + "LiraSign": "₤", + "LL": "", + "ll": "", + "logicaland": "∧", + "logicalnot": "¬", + "logicalor": "∨", + "longs": "ſ", + "lozenge": "◊", + "Lslash": "Ł", + "lslash": "ł", + "Lslashsmall": "", + "Lsmall": "", + "lsuperior": "", + "ltshade": "░", + "macron": "¯", + "macron": "ˉ", + "Macron": "", + "Macronsmall": "", + "male": "♂", + "masculine": "º", + "MillSign": "₥", + "minplus": "+", + "minus": "-", + "minus": "−", + "minute": "′", + "Msmall": "", + "msuperior": "", + "mu": "µ", + "Mu": "Μ", + "mu": "μ", + "multiply": "*", + "multiply": "×", + "musicalnote": "♪", + "musicalnotedbl": "♫", + "Nacute": "Ń", + "nacute": "ń", + "NairaSign": "₦", + "napostrophe": "ʼn", + "Ncaron": "Ň", + "ncaron": "ň", + "Ncommaaccent": "Ņ", + "ncommaaccent": "ņ", + "NewSheqelSign": "₪", + "nine": "9", + "nineinferior": "₉", + "nineoldstyle": "", + "ninesubscript": "₉", + "ninesuperior": "⁹", + "nobreakspace": " ", + "notelement": "∉", + "notequal": "≠", + "notsign": "¬", + "notsubset": "⊄", + "Nsmall": "", + "nsuperior": "ⁿ", + "Ntilde": "Ñ", + "ntilde": "ñ", + "Ntildesmall": "", + "Nu": "Ν", + "nu": "ν", + "numbersign": "#", + "numerosign": "№", + "Oacute": "Ó", + "oacute": "ó", + "Oacutesmall": "", + "Obreve": "Ŏ", + "obreve": "ŏ", + "Ocircumflex": "Ô", + "ocircumflex": "ô", + "Ocircumflexacute": "Ố", + "ocircumflexacute": "ố", + "Ocircumflexbelowdot": "Ộ", + "ocircumflexbelowdot": "ộ", + "Ocircumflexgrave": "Ồ", + "ocircumflexgrave": "ồ", + "Ocircumflexhook": "Ổ", + "ocircumflexhook": "ổ", + "Ocircumflexsmall": "", + "Ocircumflextilde": "Ỗ", + "ocircumflextilde": "ỗ", + "Odiaeresis": "Ö", + "odiaeresis": "ö", + "Odieresis": "Ö", + "odieresis": "ö", + "Odieresissmall": "", + "oe": "œ", + "OE": "Œ", + "OEsmall": "", + "ogonek": "˛", + "Ogoneksmall": "", + "Ograve": "Ò", + "ograve": "ò", + "Ogravesmall": "", + "Ohorn": "Ơ", + "ohorn": "ơ", + "Ohornacute": "Ớ", + "ohornacute": "ớ", + "Ohornbelowdot": "Ợ", + "ohornbelowdot": "ợ", + "Ohorngrave": "Ờ", + "ohorngrave": "ờ", + "Ohornhook": "Ở", + "ohornhook": "ở", + "Ohorntilde": "Ỡ", + "ohorntilde": "ỡ", + "Ohungarumlaut": "Ő", + "ohungarumlaut": "ő", + "Omacron": "Ō", + "omacron": "ō", + "Omega": "Ω", + "omega": "ω", + "omega1": "ϖ", + "Omegatonos": "Ώ", + "omegatonos": "ώ", + "Omicron": "Ο", + "omicron": "ο", + "Omicrontonos": "Ό", + "omicrontonos": "ό", + "one": "1", + "onedotenleader": "․", + "oneeighth": "⅛", + "onefitted": "", + "onehalf": "½", + "oneinferior": "₁", + "oneoldstyle": "", + "onequarter": "¼", + "onesubscript": "₁", + "onesuperior": "¹", + "onethird": "⅓", + "openbullet": "◦", + "ordfeminine": "ª", + "ordmasculine": "º", + "orthogonal": "∟", + "Oslash": "Ø", + "oslash": "ø", + "Oslashacute": "Ǿ", + "oslashacute": "ǿ", + "Oslashsmall": "", + "Osmall": "", + "osuperior": "", + "Otilde": "Õ", + "otilde": "õ", + "Otildesmall": "", + "paragraph": "¶", + "parenleft": "(", + "parenleftbt": "", + "parenleftex": "", + "parenleftinferior": "₍", + "parenleftsuperior": "⁽", + "parenlefttp": "", + "parenright": ")", + "parenrightbt": "", + "parenrightex": "", + "parenrightinferior": "₎", + "parenrightsuperior": "⁾", + "parenrighttp": "", + "partialdiff": "∂", + "percent": "%", + "period": ".", + "periodcentered": "·", + "periodcentered": "∙", + "periodinferior": "", + "periodsuperior": "", + "perpendicular": "⊥", + "perthousand": "‰", + "peseta": "₧", + "PesetaSign": "₧", + "Phi": "Φ", + "phi": "φ", + "phi1": "ϕ", + "Pi": "Π", + "pi": "π", + "plus": "+", + "plusminus": "±", + "pound": "£", + "prescription": "℞", + "product": "∏", + "propersubset": "⊂", + "propersuperset": "⊃", + "proportional": "∝", + "Psi": "Ψ", + "psi": "ψ", + "Psmall": "", + "Qsmall": "", + "question": "?", + "questiondown": "¿", + "questiondownsmall": "", + "questionsmall": "", + "quotedbl": "\"", + "quotedblbase": "„", + "quotedblleft": "“", + "quotedblright": "”", + "quoteleft": "‘", + "quotereversed": "‛", + "quoteright": "’", + "quotesinglbase": "‚", + "quotesingle": "'", + "Racute": "Ŕ", + "racute": "ŕ", + "radical": "√", + "radicalex": "", + "Rcaron": "Ř", + "rcaron": "ř", + "Rcommaaccent": "Ŗ", + "rcommaaccent": "ŗ", + "reflexsubset": "⊆", + "reflexsuperset": "⊇", + "registered": "®", + "registersans": "", + "registerserif": "", + "revlogicalnot": "⌐", + "Rfraktur": "ℜ", + "Rho": "Ρ", + "rho": "ρ", + "ring": "˚", + "Ringsmall": "", + "Rsmall": "", + "rsuperior": "", + "rtblock": "▐", + "RupeeSign": "₨", + "rupiah": "", + "Sacute": "Ś", + "sacute": "ś", + "Scaron": "Š", + "scaron": "š", + "Scaronsmall": "", + "Scedilla": "", + "scedilla": "", + "Scircumflex": "Ŝ", + "scircumflex": "ŝ", + "Scommaaccent": "Ș", + "scommaaccent": "ș", + "second": "″", + "section": "§", + "semicolon": ";", + "seven": "7", + "seveneighths": "⅞", + "seveninferior": "₇", + "sevenoldstyle": "", + "sevensubscript": "₇", + "sevensuperior": "⁷", + "SF010000": "┌", + "SF020000": "└", + "SF030000": "┐", + "SF040000": "┘", + "SF050000": "┼", + "SF060000": "┬", + "SF070000": "┴", + "SF080000": "├", + "SF090000": "┤", + "SF100000": "─", + "SF110000": "│", + "SF190000": "╡", + "SF200000": "╢", + "SF210000": "╖", + "SF220000": "╕", + "SF230000": "╣", + "SF240000": "║", + "SF250000": "╗", + "SF260000": "╝", + "SF270000": "╜", + "SF280000": "╛", + "SF360000": "╞", + "SF370000": "╟", + "SF380000": "╚", + "SF390000": "╔", + "SF400000": "╩", + "SF410000": "╦", + "SF420000": "╠", + "SF430000": "═", + "SF440000": "╬", + "SF450000": "╧", + "SF460000": "╨", + "SF470000": "╤", + "SF480000": "╥", + "SF490000": "╙", + "SF500000": "╘", + "SF510000": "╒", + "SF520000": "╓", + "SF530000": "╫", + "SF540000": "╪", + "shade": "▒", + "Sigma": "Σ", + "sigma": "σ", + "sigma1": "ς", + "similar": "∼", + "similarequal": "≃", + "six": "6", + "sixinferior": "₆", + "sixoldstyle": "", + "sixsubscript": "₆", + "sixsuperior": "⁶", + "slash": "/", + "smileface": "☺", + "spade": "♠", + "ssharp": "§", + "ssharp": "ß", + "Ssharp": "ẞ", + "Ssmall": "", + "ssuperior": "", + "sterling": "£", + "subtract": "-", + "suchthat": "∋", + "summation": "∑", + "sun": "☼", + "Tau": "Τ", + "tau": "τ", + "Tbar": "Ŧ", + "tbar": "ŧ", + "Tcaron": "Ť", + "tcaron": "ť", + "Tcommaaccent": "Ț", + "tcommaaccent": "ț", + "Thai_baht": "฿", + "therefore": "∴", + "Theta": "Θ", + "theta": "θ", + "theta1": "ϑ", + "THORN": "Þ", + "thorn": "þ", + "Thorn": "Þ", + "Thornsmall": "", + "three": "3", + "threeeighths": "⅜", + "threeinferior": "₃", + "threeoldstyle": "", + "threequarters": "¾", + "threequartersemdash": "", + "threesubscript": "₃", + "threesuperior": "³", + "til": "~", + "tilde": "~", + "tilde": "˜", + "tildecomb": "̃", + "Tildesmall": "", + "tonos": "΄", + "trademark": "™", + "trademarksans": "", + "trademarkserif": "", + "triagdn": "▼", + "triaglf": "◄", + "triagrt": "►", + "triagup": "▲", + "Tsmall": "", + "tsuperior": "", + "two": "2", + "twodotenleader": "‥", + "twoinferior": "₂", + "twooldstyle": "", + "twosubscript": "₂", + "twosuperior": "²", + "twothirds": "⅔", + "Uacute": "Ú", + "uacute": "ú", + "Uacutesmall": "", + "Ubreve": "Ŭ", + "ubreve": "ŭ", + "Ucircumflex": "Û", + "ucircumflex": "û", + "Ucircumflexsmall": "", + "Udiaeresis": "Ü", + "udiaeresis": "ü", + "Udieresis": "Ü", + "udieresis": "ü", + "Udieresissmall": "", + "Ugrave": "Ù", + "ugrave": "ù", + "Ugravesmall": "", + "Uhorn": "Ư", + "uhorn": "ư", + "Uhornacute": "Ứ", + "uhornacute": "ứ", + "Uhornbelowdot": "Ự", + "uhornbelowdot": "ự", + "Uhorngrave": "Ừ", + "uhorngrave": "ừ", + "Uhornhook": "Ử", + "uhornhook": "ử", + "Uhorntilde": "Ữ", + "uhorntilde": "ữ", + "Uhungarumlaut": "Ű", + "uhungarumlaut": "ű", + "Umacron": "Ū", + "umacron": "ū", + "underscore": "_", + "underscoredbl": "‗", + "union": "∪", + "universal": "∀", + "Uogonek": "Ų", + "uogonek": "ų", + "upblock": "▀", + "Upsilon": "Υ", + "upsilon": "υ", + "Upsilon1": "ϒ", + "Upsilondieresis": "Ϋ", + "upsilondieresis": "ϋ", + "upsilondieresistonos": "ΰ", + "Upsilontonos": "Ύ", + "upsilontonos": "ύ", + "Uring": "Ů", + "uring": "ů", + "Usmall": "", + "Utilde": "Ũ", + "utilde": "ũ", + "Vsmall": "", + "Wacute": "Ẃ", + "wacute": "ẃ", + "Wcircumflex": "Ŵ", + "wcircumflex": "ŵ", + "Wdieresis": "Ẅ", + "wdieresis": "ẅ", + "weierstrass": "℘", + "Wgrave": "Ẁ", + "wgrave": "ẁ", + "WonSign": "₩", + "Wsmall": "", + "Xi": "Ξ", + "xi": "ξ", + "Xsmall": "", + "Yacute": "Ý", + "yacute": "ý", + "Yacutesmall": "", + "Ycircumflex": "Ŷ", + "ycircumflex": "ŷ", + "ydiaeresis": "ÿ", + "Ydieresis": "Ÿ", + "ydieresis": "ÿ", + "Ydieresissmall": "", + "yen": "¥", + "Ygrave": "Ỳ", + "ygrave": "ỳ", + "Ysmall": "", + "Zacute": "Ź", + "zacute": "ź", + "Zcaron": "Ž", + "zcaron": "ž", + "Zcaronsmall": "", + "Zdotaccent": "Ż", + "zdotaccent": "ż", + "zero": "0", + "zeroinferior": "₀", + "zerooldstyle": "", + "zerosubscript": "₀", + "zerosuperior": "⁰", + "zeta": "ζ", + "Zeta": "Ζ", + "Zsmall": "", +} +sided_modifiers = {'ctrl', 'alt', 'shift', 'windows'} +all_modifiers = {'alt', 'alt gr', 'ctrl', 'shift', 'windows'} | set('left ' + n for n in sided_modifiers) | set('right ' + n for n in sided_modifiers) + +# Platform-specific canonical overrides + +if platform.system() == 'Darwin': + canonical_names.update({ + "command": "command", + "windows": "command", + "cmd": "command", + "win": "command", + "backspace": "delete", + 'alt gr': 'alt' # Issue #117 + }) + all_modifiers = {'alt', 'ctrl', 'shift', 'windows'} +if platform.system() == 'Linux': + canonical_names.update({ + "select": "end", + "find": "home", + 'next': 'page down', + 'prior': 'page up', + }) + +def normalize_name(name): + """ + Given a key name (e.g. "LEFT CONTROL"), clean up the string and convert to + the canonical representation (e.g. "left ctrl") if one is known. + """ + if not name or not isinstance(name, basestring): + raise ValueError('Can only normalize non-empty string names. Unexpected '+ repr(name)) + + if len(name) > 1: + name = name.lower() + if name != '_' and '_' in name: + name = name.replace('_', ' ') + + return canonical_names.get(name, name) diff --git a/keyboard/_darwinkeyboard.py b/keyboard/_darwinkeyboard.py new file mode 100644 index 0000000..85670ee --- /dev/null +++ b/keyboard/_darwinkeyboard.py @@ -0,0 +1,442 @@ +import ctypes +import ctypes.util +import Quartz +import time +import os +import threading +from AppKit import NSEvent +from ._keyboard_event import KeyboardEvent, KEY_DOWN, KEY_UP +from ._canonical_names import normalize_name + +try: # Python 2/3 compatibility + unichr +except NameError: + unichr = chr + +Carbon = ctypes.cdll.LoadLibrary(ctypes.util.find_library('Carbon')) + +class KeyMap(object): + non_layout_keys = dict((vk, normalize_name(name)) for vk, name in { + # Layout specific keys from https://stackoverflow.com/a/16125341/252218 + # Unfortunately no source for layout-independent keys was found. + 0x24: 'return', + 0x30: 'tab', + 0x31: 'space', + 0x33: 'delete', + 0x35: 'escape', + 0x37: 'command', + 0x38: 'shift', + 0x39: 'capslock', + 0x3a: 'option', + 0x3b: 'control', + 0x3c: 'right shift', + 0x3d: 'right option', + 0x3e: 'right control', + 0x3f: 'function', + 0x40: 'f17', + 0x48: 'volume up', + 0x49: 'volume down', + 0x4a: 'mute', + 0x4f: 'f18', + 0x50: 'f19', + 0x5a: 'f20', + 0x60: 'f5', + 0x61: 'f6', + 0x62: 'f7', + 0x63: 'f3', + 0x64: 'f8', + 0x65: 'f9', + 0x67: 'f11', + 0x69: 'f13', + 0x6a: 'f16', + 0x6b: 'f14', + 0x6d: 'f10', + 0x6f: 'f12', + 0x71: 'f15', + 0x72: 'help', + 0x73: 'home', + 0x74: 'page up', + 0x75: 'forward delete', + 0x76: 'f4', + 0x77: 'end', + 0x78: 'f2', + 0x79: 'page down', + 0x7a: 'f1', + 0x7b: 'left', + 0x7c: 'right', + 0x7d: 'down', + 0x7e: 'up', + }.items()) + layout_specific_keys = {} + def __init__(self): + # Virtual key codes are usually the same for any given key, unless you have a different + # keyboard layout. The only way I've found to determine the layout relies on (supposedly + # deprecated) Carbon APIs. If there's a more modern way to do this, please update this + # section. + + # Set up data types and exported values: + + CFTypeRef = ctypes.c_void_p + CFDataRef = ctypes.c_void_p + CFIndex = ctypes.c_uint64 + OptionBits = ctypes.c_uint32 + UniCharCount = ctypes.c_uint8 + UniChar = ctypes.c_uint16 + UniChar4 = UniChar * 4 + + class CFRange(ctypes.Structure): + _fields_ = [('loc', CFIndex), + ('len', CFIndex)] + + kTISPropertyUnicodeKeyLayoutData = ctypes.c_void_p.in_dll(Carbon, 'kTISPropertyUnicodeKeyLayoutData') + shiftKey = 0x0200 + alphaKey = 0x0400 + optionKey = 0x0800 + controlKey = 0x1000 + kUCKeyActionDisplay = 3 + kUCKeyTranslateNoDeadKeysBit = 0 + + # Set up function calls: + Carbon.CFDataGetBytes.argtypes = [CFDataRef] #, CFRange, UInt8 + Carbon.CFDataGetBytes.restype = None + Carbon.CFDataGetLength.argtypes = [CFDataRef] + Carbon.CFDataGetLength.restype = CFIndex + Carbon.CFRelease.argtypes = [CFTypeRef] + Carbon.CFRelease.restype = None + Carbon.LMGetKbdType.argtypes = [] + Carbon.LMGetKbdType.restype = ctypes.c_uint32 + Carbon.TISCopyCurrentKeyboardInputSource.argtypes = [] + Carbon.TISCopyCurrentKeyboardInputSource.restype = ctypes.c_void_p + Carbon.TISCopyCurrentASCIICapableKeyboardLayoutInputSource.argtypes = [] + Carbon.TISCopyCurrentASCIICapableKeyboardLayoutInputSource.restype = ctypes.c_void_p + Carbon.TISGetInputSourceProperty.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + Carbon.TISGetInputSourceProperty.restype = ctypes.c_void_p + Carbon.UCKeyTranslate.argtypes = [ctypes.c_void_p, + ctypes.c_uint16, + ctypes.c_uint16, + ctypes.c_uint32, + ctypes.c_uint32, + OptionBits, # keyTranslateOptions + ctypes.POINTER(ctypes.c_uint32), # deadKeyState + UniCharCount, # maxStringLength + ctypes.POINTER(UniCharCount), # actualStringLength + UniChar4] + Carbon.UCKeyTranslate.restype = ctypes.c_uint32 + + # Get keyboard layout + klis = Carbon.TISCopyCurrentKeyboardInputSource() + k_layout = Carbon.TISGetInputSourceProperty(klis, kTISPropertyUnicodeKeyLayoutData) + if k_layout is None: + klis = Carbon.TISCopyCurrentASCIICapableKeyboardLayoutInputSource() + k_layout = Carbon.TISGetInputSourceProperty(klis, kTISPropertyUnicodeKeyLayoutData) + k_layout_size = Carbon.CFDataGetLength(k_layout) + k_layout_buffer = ctypes.create_string_buffer(k_layout_size) # TODO - Verify this works instead of initializing with empty string + Carbon.CFDataGetBytes(k_layout, CFRange(0, k_layout_size), ctypes.byref(k_layout_buffer)) + + # Generate character representations of key codes + for key_code in range(0, 128): + # TODO - Possibly add alt modifier to key map + non_shifted_char = UniChar4() + shifted_char = UniChar4() + keys_down = ctypes.c_uint32() + char_count = UniCharCount() + + retval = Carbon.UCKeyTranslate(k_layout_buffer, + key_code, + kUCKeyActionDisplay, + 0, # No modifier + Carbon.LMGetKbdType(), + kUCKeyTranslateNoDeadKeysBit, + ctypes.byref(keys_down), + 4, + ctypes.byref(char_count), + non_shifted_char) + + non_shifted_key = u''.join(unichr(non_shifted_char[i]) for i in range(char_count.value)) + + retval = Carbon.UCKeyTranslate(k_layout_buffer, + key_code, + kUCKeyActionDisplay, + shiftKey >> 8, # Shift + Carbon.LMGetKbdType(), + kUCKeyTranslateNoDeadKeysBit, + ctypes.byref(keys_down), + 4, + ctypes.byref(char_count), + shifted_char) + + shifted_key = u''.join(unichr(shifted_char[i]) for i in range(char_count.value)) + + self.layout_specific_keys[key_code] = (non_shifted_key, shifted_key) + # Cleanup + Carbon.CFRelease(klis) + + def character_to_vk(self, character): + """ Returns a tuple of (scan_code, modifiers) where ``scan_code`` is a numeric scan code + and ``modifiers`` is an array of string modifier names (like 'shift') """ + for vk in self.non_layout_keys: + if self.non_layout_keys[vk] == character.lower(): + return (vk, []) + for vk in self.layout_specific_keys: + if self.layout_specific_keys[vk][0] == character: + return (vk, []) + elif self.layout_specific_keys[vk][1] == character: + return (vk, ['shift']) + raise ValueError("Unrecognized character: {}".format(character)) + + def vk_to_character(self, vk, modifiers=[]): + """ Returns a character corresponding to the specified scan code (with given + modifiers applied) """ + if vk in self.non_layout_keys: + # Not a character + return self.non_layout_keys[vk] + elif vk in self.layout_specific_keys: + if 'shift' in modifiers: + return self.layout_specific_keys[vk][1] + return self.layout_specific_keys[vk][0] + else: + # Invalid vk + raise ValueError("Invalid scan code: {}".format(vk)) + + +class KeyController(object): + def __init__(self): + self.key_map = KeyMap() + self.current_modifiers = { + "shift": False, + "caps": False, + "alt": False, + "ctrl": False, + "cmd": False, + } + self.media_keys = { + 'KEYTYPE_SOUND_UP': 0, + 'KEYTYPE_SOUND_DOWN': 1, + 'KEYTYPE_BRIGHTNESS_UP': 2, + 'KEYTYPE_BRIGHTNESS_DOWN': 3, + 'KEYTYPE_CAPS_LOCK': 4, + 'KEYTYPE_HELP': 5, + 'POWER_KEY': 6, + 'KEYTYPE_MUTE': 7, + 'UP_ARROW_KEY': 8, + 'DOWN_ARROW_KEY': 9, + 'KEYTYPE_NUM_LOCK': 10, + 'KEYTYPE_CONTRAST_UP': 11, + 'KEYTYPE_CONTRAST_DOWN': 12, + 'KEYTYPE_LAUNCH_PANEL': 13, + 'KEYTYPE_EJECT': 14, + 'KEYTYPE_VIDMIRROR': 15, + 'KEYTYPE_PLAY': 16, + 'KEYTYPE_NEXT': 17, + 'KEYTYPE_PREVIOUS': 18, + 'KEYTYPE_FAST': 19, + 'KEYTYPE_REWIND': 20, + 'KEYTYPE_ILLUMINATION_UP': 21, + 'KEYTYPE_ILLUMINATION_DOWN': 22, + 'KEYTYPE_ILLUMINATION_TOGGLE': 23 + } + + def press(self, key_code): + """ Sends a 'down' event for the specified scan code """ + if key_code >= 128: + # Media key + ev = NSEvent.otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2_( + 14, # type + (0, 0), # location + 0xa00, # flags + 0, # timestamp + 0, # window + 0, # ctx + 8, # subtype + ((key_code-128) << 16) | (0xa << 8), # data1 + -1 # data2 + ) + Quartz.CGEventPost(0, ev.CGEvent()) + else: + # Regular key + # Apply modifiers if necessary + event_flags = 0 + if self.current_modifiers["shift"]: + event_flags += Quartz.kCGEventFlagMaskShift + if self.current_modifiers["caps"]: + event_flags += Quartz.kCGEventFlagMaskAlphaShift + if self.current_modifiers["alt"]: + event_flags += Quartz.kCGEventFlagMaskAlternate + if self.current_modifiers["ctrl"]: + event_flags += Quartz.kCGEventFlagMaskControl + if self.current_modifiers["cmd"]: + event_flags += Quartz.kCGEventFlagMaskCommand + + # Update modifiers if necessary + if key_code == 0x37: # cmd + self.current_modifiers["cmd"] = True + elif key_code == 0x38 or key_code == 0x3C: # shift or right shift + self.current_modifiers["shift"] = True + elif key_code == 0x39: # caps lock + self.current_modifiers["caps"] = True + elif key_code == 0x3A: # alt + self.current_modifiers["alt"] = True + elif key_code == 0x3B: # ctrl + self.current_modifiers["ctrl"] = True + event = Quartz.CGEventCreateKeyboardEvent(None, key_code, True) + Quartz.CGEventSetFlags(event, event_flags) + Quartz.CGEventPost(Quartz.kCGHIDEventTap, event) + time.sleep(0.01) + + def release(self, key_code): + """ Sends an 'up' event for the specified scan code """ + if key_code >= 128: + # Media key + ev = NSEvent.otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2_( + 14, # type + (0, 0), # location + 0xb00, # flags + 0, # timestamp + 0, # window + 0, # ctx + 8, # subtype + ((key_code-128) << 16) | (0xb << 8), # data1 + -1 # data2 + ) + Quartz.CGEventPost(0, ev.CGEvent()) + else: + # Regular key + # Update modifiers if necessary + if key_code == 0x37: # cmd + self.current_modifiers["cmd"] = False + elif key_code == 0x38 or key_code == 0x3C: # shift or right shift + self.current_modifiers["shift"] = False + elif key_code == 0x39: # caps lock + self.current_modifiers["caps"] = False + elif key_code == 0x3A: # alt + self.current_modifiers["alt"] = False + elif key_code == 0x3B: # ctrl + self.current_modifiers["ctrl"] = False + + # Apply modifiers if necessary + event_flags = 0 + if self.current_modifiers["shift"]: + event_flags += Quartz.kCGEventFlagMaskShift + if self.current_modifiers["caps"]: + event_flags += Quartz.kCGEventFlagMaskAlphaShift + if self.current_modifiers["alt"]: + event_flags += Quartz.kCGEventFlagMaskAlternate + if self.current_modifiers["ctrl"]: + event_flags += Quartz.kCGEventFlagMaskControl + if self.current_modifiers["cmd"]: + event_flags += Quartz.kCGEventFlagMaskCommand + event = Quartz.CGEventCreateKeyboardEvent(None, key_code, False) + Quartz.CGEventSetFlags(event, event_flags) + Quartz.CGEventPost(Quartz.kCGHIDEventTap, event) + time.sleep(0.01) + + def map_char(self, character): + if character in self.media_keys: + return (128+self.media_keys[character],[]) + else: + return self.key_map.character_to_vk(character) + def map_scan_code(self, scan_code): + if scan_code >= 128: + character = [k for k, v in enumerate(self.media_keys) if v == scan_code-128] + if len(character): + return character[0] + return None + else: + return self.key_map.vk_to_character(scan_code) + +class KeyEventListener(object): + def __init__(self, callback, blocking=False): + self.blocking = blocking + self.callback = callback + self.listening = True + self.tap = None + + def run(self): + """ Creates a listener and loops while waiting for an event. Intended to run as + a background thread. """ + self.tap = Quartz.CGEventTapCreate( + Quartz.kCGSessionEventTap, + Quartz.kCGHeadInsertEventTap, + Quartz.kCGEventTapOptionDefault, + Quartz.CGEventMaskBit(Quartz.kCGEventKeyDown) | + Quartz.CGEventMaskBit(Quartz.kCGEventKeyUp) | + Quartz.CGEventMaskBit(Quartz.kCGEventFlagsChanged), + self.handler, + None) + loopsource = Quartz.CFMachPortCreateRunLoopSource(None, self.tap, 0) + loop = Quartz.CFRunLoopGetCurrent() + Quartz.CFRunLoopAddSource(loop, loopsource, Quartz.kCFRunLoopDefaultMode) + Quartz.CGEventTapEnable(self.tap, True) + + while self.listening: + Quartz.CFRunLoopRunInMode(Quartz.kCFRunLoopDefaultMode, 5, False) + + def handler(self, proxy, e_type, event, refcon): + scan_code = Quartz.CGEventGetIntegerValueField(event, Quartz.kCGKeyboardEventKeycode) + key_name = name_from_scancode(scan_code) + flags = Quartz.CGEventGetFlags(event) + event_type = "" + is_keypad = (flags & Quartz.kCGEventFlagMaskNumericPad) + if e_type == Quartz.kCGEventKeyDown: + event_type = "down" + elif e_type == Quartz.kCGEventKeyUp: + event_type = "up" + elif e_type == Quartz.kCGEventFlagsChanged: + if key_name.endswith("shift") and (flags & Quartz.kCGEventFlagMaskShift): + event_type = "down" + elif key_name == "caps lock" and (flags & Quartz.kCGEventFlagMaskAlphaShift): + event_type = "down" + elif (key_name.endswith("option") or key_name.endswith("alt")) and (flags & Quartz.kCGEventFlagMaskAlternate): + event_type = "down" + elif key_name == "ctrl" and (flags & Quartz.kCGEventFlagMaskControl): + event_type = "down" + elif key_name == "command" and (flags & Quartz.kCGEventFlagMaskCommand): + event_type = "down" + else: + event_type = "up" + + if self.blocking: + return None + + self.callback(KeyboardEvent(event_type, scan_code, name=key_name, is_keypad=is_keypad)) + return event + +key_controller = KeyController() + +""" Exported functions below """ + +def init(): + key_controller = KeyController() + +def press(scan_code): + """ Sends a 'down' event for the specified scan code """ + key_controller.press(scan_code) + +def release(scan_code): + """ Sends an 'up' event for the specified scan code """ + key_controller.release(scan_code) + +def map_name(name): + """ Returns a tuple of (scan_code, modifiers) where ``scan_code`` is a numeric scan code + and ``modifiers`` is an array of string modifier names (like 'shift') """ + yield key_controller.map_char(name) + +def name_from_scancode(scan_code): + """ Returns the name or character associated with the specified key code """ + return key_controller.map_scan_code(scan_code) + +def listen(callback): + if not os.geteuid() == 0: + raise OSError("Error 13 - Must be run as administrator") + KeyEventListener(callback).run() + +def type_unicode(character): + OUTPUT_SOURCE = Quartz.CGEventSourceCreate(Quartz.kCGEventSourceStateHIDSystemState) + # Key down + event = Quartz.CGEventCreateKeyboardEvent(OUTPUT_SOURCE, 0, True) + Quartz.CGEventKeyboardSetUnicodeString(event, len(character.encode('utf-16-le')) // 2, character) + Quartz.CGEventPost(Quartz.kCGSessionEventTap, event) + # Key up + event = Quartz.CGEventCreateKeyboardEvent(OUTPUT_SOURCE, 0, False) + Quartz.CGEventKeyboardSetUnicodeString(event, len(character.encode('utf-16-le')) // 2, character) + Quartz.CGEventPost(Quartz.kCGSessionEventTap, event) \ No newline at end of file diff --git a/keyboard/_darwinmouse.py b/keyboard/_darwinmouse.py new file mode 100644 index 0000000..b112dc0 --- /dev/null +++ b/keyboard/_darwinmouse.py @@ -0,0 +1,173 @@ +import os +import datetime +import threading +import Quartz +from ._mouse_event import ButtonEvent, WheelEvent, MoveEvent, LEFT, RIGHT, MIDDLE, X, X2, UP, DOWN + +_button_mapping = { + LEFT: (Quartz.kCGMouseButtonLeft, Quartz.kCGEventLeftMouseDown, Quartz.kCGEventLeftMouseUp, Quartz.kCGEventLeftMouseDragged), + RIGHT: (Quartz.kCGMouseButtonRight, Quartz.kCGEventRightMouseDown, Quartz.kCGEventRightMouseUp, Quartz.kCGEventRightMouseDragged), + MIDDLE: (Quartz.kCGMouseButtonCenter, Quartz.kCGEventOtherMouseDown, Quartz.kCGEventOtherMouseUp, Quartz.kCGEventOtherMouseDragged) +} +_button_state = { + LEFT: False, + RIGHT: False, + MIDDLE: False +} +_last_click = { + "time": None, + "button": None, + "position": None, + "click_count": 0 +} + +class MouseEventListener(object): + def __init__(self, callback, blocking=False): + self.blocking = blocking + self.callback = callback + self.listening = True + + def run(self): + """ Creates a listener and loops while waiting for an event. Intended to run as + a background thread. """ + self.tap = Quartz.CGEventTapCreate( + Quartz.kCGSessionEventTap, + Quartz.kCGHeadInsertEventTap, + Quartz.kCGEventTapOptionDefault, + Quartz.CGEventMaskBit(Quartz.kCGEventLeftMouseDown) | + Quartz.CGEventMaskBit(Quartz.kCGEventLeftMouseUp) | + Quartz.CGEventMaskBit(Quartz.kCGEventRightMouseDown) | + Quartz.CGEventMaskBit(Quartz.kCGEventRightMouseUp) | + Quartz.CGEventMaskBit(Quartz.kCGEventOtherMouseDown) | + Quartz.CGEventMaskBit(Quartz.kCGEventOtherMouseUp) | + Quartz.CGEventMaskBit(Quartz.kCGEventMouseMoved) | + Quartz.CGEventMaskBit(Quartz.kCGEventScrollWheel), + self.handler, + None) + loopsource = Quartz.CFMachPortCreateRunLoopSource(None, self.tap, 0) + loop = Quartz.CFRunLoopGetCurrent() + Quartz.CFRunLoopAddSource(loop, loopsource, Quartz.kCFRunLoopDefaultMode) + Quartz.CGEventTapEnable(self.tap, True) + + while self.listening: + Quartz.CFRunLoopRunInMode(Quartz.kCFRunLoopDefaultMode, 5, False) + + def handler(self, proxy, e_type, event, refcon): + # TODO Separate event types by button/wheel/move + scan_code = Quartz.CGEventGetIntegerValueField(event, Quartz.kCGKeyboardEventKeycode) + key_name = name_from_scancode(scan_code) + flags = Quartz.CGEventGetFlags(event) + event_type = "" + is_keypad = (flags & Quartz.kCGEventFlagMaskNumericPad) + if e_type == Quartz.kCGEventKeyDown: + event_type = "down" + elif e_type == Quartz.kCGEventKeyUp: + event_type = "up" + + if self.blocking: + return None + + self.callback(KeyboardEvent(event_type, scan_code, name=key_name, is_keypad=is_keypad)) + return event + +# Exports + +def init(): + """ Initializes mouse state """ + pass + +def listen(queue): + """ Appends events to the queue (ButtonEvent, WheelEvent, and MoveEvent). """ + if not os.geteuid() == 0: + raise OSError("Error 13 - Must be run as administrator") + listener = MouseEventListener(lambda e: queue.put(e) or is_allowed(e.name, e.event_type == KEY_UP)) + t = threading.Thread(target=listener.run, args=()) + t.daemon = True + t.start() + +def press(button=LEFT): + """ Sends a down event for the specified button, using the provided constants """ + location = get_position() + button_code, button_down, _, _ = _button_mapping[button] + e = Quartz.CGEventCreateMouseEvent( + None, + button_down, + location, + button_code) + + # Check if this is a double-click (same location within the last 300ms) + if _last_click["time"] is not None and datetime.datetime.now() - _last_click["time"] < datetime.timedelta(seconds=0.3) and _last_click["button"] == button and _last_click["position"] == location: + # Repeated Click + _last_click["click_count"] = min(3, _last_click["click_count"]+1) + else: + # Not a double-click - Reset last click + _last_click["click_count"] = 1 + Quartz.CGEventSetIntegerValueField( + e, + Quartz.kCGMouseEventClickState, + _last_click["click_count"]) + Quartz.CGEventPost(Quartz.kCGHIDEventTap, e) + _button_state[button] = True + _last_click["time"] = datetime.datetime.now() + _last_click["button"] = button + _last_click["position"] = location + +def release(button=LEFT): + """ Sends an up event for the specified button, using the provided constants """ + location = get_position() + button_code, _, button_up, _ = _button_mapping[button] + e = Quartz.CGEventCreateMouseEvent( + None, + button_up, + location, + button_code) + + if _last_click["time"] is not None and _last_click["time"] > datetime.datetime.now() - datetime.timedelta(microseconds=300000) and _last_click["button"] == button and _last_click["position"] == location: + # Repeated Click + Quartz.CGEventSetIntegerValueField( + e, + Quartz.kCGMouseEventClickState, + _last_click["click_count"]) + Quartz.CGEventPost(Quartz.kCGHIDEventTap, e) + _button_state[button] = False + +def wheel(delta=1): + """ Sends a wheel event for the provided number of clicks. May be negative to reverse + direction. """ + location = get_position() + e = Quartz.CGEventCreateMouseEvent( + None, + Quartz.kCGEventScrollWheel, + location, + Quartz.kCGMouseButtonLeft) + e2 = Quartz.CGEventCreateScrollWheelEvent( + None, + Quartz.kCGScrollEventUnitLine, + 1, + delta) + Quartz.CGEventPost(Quartz.kCGHIDEventTap, e) + Quartz.CGEventPost(Quartz.kCGHIDEventTap, e2) + +def move_to(x, y): + """ Sets the mouse's location to the specified coordinates. """ + for b in _button_state: + if _button_state[b]: + e = Quartz.CGEventCreateMouseEvent( + None, + _button_mapping[b][3], # Drag Event + (x, y), + _button_mapping[b][0]) + break + else: + e = Quartz.CGEventCreateMouseEvent( + None, + Quartz.kCGEventMouseMoved, + (x, y), + Quartz.kCGMouseButtonLeft) + Quartz.CGEventPost(Quartz.kCGHIDEventTap, e) + +def get_position(): + """ Returns the mouse's location as a tuple of (x, y). """ + e = Quartz.CGEventCreate(None) + point = Quartz.CGEventGetLocation(e) + return (point.x, point.y) \ No newline at end of file diff --git a/keyboard/_generic.py b/keyboard/_generic.py new file mode 100644 index 0000000..762858d --- /dev/null +++ b/keyboard/_generic.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +from threading import Thread, Lock +import traceback +import functools + +try: + from queue import Queue +except ImportError: + from Queue import Queue + +class GenericListener(object): + lock = Lock() + + def __init__(self): + self.handlers = [] + self.listening = False + self.queue = Queue() + + def invoke_handlers(self, event): + for handler in self.handlers: + try: + if handler(event): + # Stop processing this hotkey. + return 1 + except Exception as e: + traceback.print_exc() + + def start_if_necessary(self): + """ + Starts the listening thread if it wans't already. + """ + self.lock.acquire() + try: + if not self.listening: + self.init() + + self.listening = True + self.listening_thread = Thread(target=self.listen) + self.listening_thread.daemon = True + self.listening_thread.start() + + self.processing_thread = Thread(target=self.process) + self.processing_thread.daemon = True + self.processing_thread.start() + finally: + self.lock.release() + + def pre_process_event(self, event): + raise NotImplementedError('This method should be implemented in the child class.') + + def process(self): + """ + Loops over the underlying queue of events and processes them in order. + """ + assert self.queue is not None + while True: + event = self.queue.get() + if self.pre_process_event(event): + self.invoke_handlers(event) + self.queue.task_done() + + def add_handler(self, handler): + """ + Adds a function to receive each event captured, starting the capturing + process if necessary. + """ + self.start_if_necessary() + self.handlers.append(handler) + + def remove_handler(self, handler): + """ Removes a previously added event handler. """ + while handler in self.handlers: + self.handlers.remove(handler) diff --git a/keyboard/_keyboard_event.py b/keyboard/_keyboard_event.py new file mode 100644 index 0000000..6d9f279 --- /dev/null +++ b/keyboard/_keyboard_event.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +from time import time as now +import json +from ._canonical_names import canonical_names, normalize_name + +try: + basestring +except NameError: + basestring = str + +KEY_DOWN = 'down' +KEY_UP = 'up' + +class KeyboardEvent(object): + event_type = None + scan_code = None + name = None + time = None + device = None + modifiers = None + is_keypad = None + + def __init__(self, event_type, scan_code, name=None, time=None, device=None, modifiers=None, is_keypad=None): + self.event_type = event_type + self.scan_code = scan_code + self.time = now() if time is None else time + self.device = device + self.is_keypad = is_keypad + self.modifiers = modifiers + if name: + self.name = normalize_name(name) + + def to_json(self, ensure_ascii=False): + attrs = dict( + (attr, getattr(self, attr)) for attr in ['event_type', 'scan_code', 'name', 'time', 'device', 'is_keypad'] + if not attr.startswith('_') and getattr(self, attr) is not None + ) + return json.dumps(attrs, ensure_ascii=ensure_ascii) + + def __repr__(self): + return 'KeyboardEvent({} {})'.format(self.name or 'Unknown {}'.format(self.scan_code), self.event_type) + + def __eq__(self, other): + return ( + isinstance(other, KeyboardEvent) + and self.event_type == other.event_type + and ( + not self.scan_code or not other.scan_code or self.scan_code == other.scan_code + ) and ( + not self.name or not other.name or self.name == other.name + ) + ) diff --git a/keyboard/_keyboard_tests.py b/keyboard/_keyboard_tests.py new file mode 100644 index 0000000..f0e1432 --- /dev/null +++ b/keyboard/_keyboard_tests.py @@ -0,0 +1,827 @@ +# -*- coding: utf-8 -*- +""" +Side effects are avoided using two techniques: + +- Low level OS requests (keyboard._os_keyboard) are mocked out by rewriting +the functions at that namespace. This includes a list of dummy keys. +- Events are pumped manually by the main test class, and accepted events +are tested against expected values. + +Fake user events are appended to `input_events`, passed through +keyboard,_listener.direct_callback, then, if accepted, appended to +`output_events`. Fake OS events (keyboard.press) are processed +and added to `output_events` immediately, mimicking real functionality. +""" +from __future__ import print_function + +import unittest +import time + +import keyboard +from ._keyboard_event import KeyboardEvent, KEY_DOWN, KEY_UP + +dummy_keys = { + 'space': [(0, [])], + + 'a': [(1, [])], + 'b': [(2, [])], + 'c': [(3, [])], + 'A': [(1, ['shift']), (-1, [])], + 'B': [(2, ['shift']), (-2, [])], + 'C': [(3, ['shift']), (-3, [])], + + 'alt': [(4, [])], + 'left alt': [(4, [])], + + 'left shift': [(5, [])], + 'right shift': [(6, [])], + + 'left ctrl': [(7, [])], + + 'backspace': [(8, [])], + 'caps lock': [(9, [])], + + '+': [(10, [])], + ',': [(11, [])], + '_': [(12, [])], + + 'none': [], + 'duplicated': [(20, []), (20, [])], +} + +def make_event(event_type, name, scan_code=None, time=0): + return KeyboardEvent(event_type=event_type, scan_code=scan_code or dummy_keys[name][0][0], name=name, time=time) + +# Used when manually pumping events. +input_events = [] +output_events = [] + +def send_instant_event(event): + if keyboard._listener.direct_callback(event): + output_events.append(event) + +# Mock out side effects. +keyboard._os_keyboard.init = lambda: None +keyboard._os_keyboard.listen = lambda callback: None +keyboard._os_keyboard.map_name = dummy_keys.__getitem__ +keyboard._os_keyboard.press = lambda scan_code: send_instant_event(make_event(KEY_DOWN, None, scan_code)) +keyboard._os_keyboard.release = lambda scan_code: send_instant_event(make_event(KEY_UP, None, scan_code)) +keyboard._os_keyboard.type_unicode = lambda char: output_events.append(KeyboardEvent(event_type=KEY_DOWN, scan_code=999, name=char)) + +# Shortcuts for defining test inputs and expected outputs. +# Usage: d_shift + d_a + u_a + u_shift +d_a = [make_event(KEY_DOWN, 'a')] +u_a = [make_event(KEY_UP, 'a')] +du_a = d_a+u_a +d_b = [make_event(KEY_DOWN, 'b')] +u_b = [make_event(KEY_UP, 'b')] +du_b = d_b+u_b +d_c = [make_event(KEY_DOWN, 'c')] +u_c = [make_event(KEY_UP, 'c')] +du_c = d_c+u_c +d_ctrl = [make_event(KEY_DOWN, 'left ctrl')] +u_ctrl = [make_event(KEY_UP, 'left ctrl')] +du_ctrl = d_ctrl+u_ctrl +d_shift = [make_event(KEY_DOWN, 'left shift')] +u_shift = [make_event(KEY_UP, 'left shift')] +du_shift = d_shift+u_shift +d_alt = [make_event(KEY_DOWN, 'alt')] +u_alt = [make_event(KEY_UP, 'alt')] +du_alt = d_alt+u_alt +du_backspace = [make_event(KEY_DOWN, 'backspace'), make_event(KEY_UP, 'backspace')] +du_capslock = [make_event(KEY_DOWN, 'caps lock'), make_event(KEY_UP, 'caps lock')] +d_space = [make_event(KEY_DOWN, 'space')] +u_space = [make_event(KEY_UP, 'space')] +du_space = [make_event(KEY_DOWN, 'space'), make_event(KEY_UP, 'space')] + +trigger = lambda e=None: keyboard.press(999) +triggered_event = [KeyboardEvent(KEY_DOWN, scan_code=999)] + +class TestKeyboard(unittest.TestCase): + def tearDown(self): + keyboard.unhook_all() + #self.assertEquals(keyboard._hooks, {}) + #self.assertEquals(keyboard._hotkeys, {}) + + def setUp(self): + #keyboard._hooks.clear() + #keyboard._hotkeys.clear() + del input_events[:] + del output_events[:] + keyboard._recording = None + keyboard._pressed_events.clear() + keyboard._physically_pressed_keys.clear() + keyboard._logically_pressed_keys.clear() + keyboard._hotkeys.clear() + keyboard._listener.init() + keyboard._word_listeners = {} + + def do(self, manual_events, expected=None): + input_events.extend(manual_events) + while input_events: + event = input_events.pop(0) + if keyboard._listener.direct_callback(event): + output_events.append(event) + if expected is not None: + to_names = lambda es: '+'.join(('d' if e.event_type == KEY_DOWN else 'u') + '_' + str(e.scan_code) for e in es) + self.assertEqual(to_names(output_events), to_names(expected)) + del output_events[:] + + keyboard._listener.queue.join() + + def test_event_json(self): + event = make_event(KEY_DOWN, u'á \'"', 999) + import json + self.assertEqual(event, KeyboardEvent(**json.loads(event.to_json()))) + + def test_is_modifier_name(self): + for name in keyboard.all_modifiers: + self.assertTrue(keyboard.is_modifier(name)) + def test_is_modifier_scan_code(self): + for i in range(10): + self.assertEqual(keyboard.is_modifier(i), i in [4, 5, 6, 7]) + + def test_key_to_scan_codes_brute(self): + for name, entries in dummy_keys.items(): + if name in ['none', 'duplicated']: continue + expected = tuple(scan_code for scan_code, modifiers in entries) + self.assertEqual(keyboard.key_to_scan_codes(name), expected) + def test_key_to_scan_code_from_scan_code(self): + for i in range(10): + self.assertEqual(keyboard.key_to_scan_codes(i), (i,)) + def test_key_to_scan_code_from_letter(self): + self.assertEqual(keyboard.key_to_scan_codes('a'), (1,)) + self.assertEqual(keyboard.key_to_scan_codes('A'), (1,-1)) + def test_key_to_scan_code_from_normalized(self): + self.assertEqual(keyboard.key_to_scan_codes('shift'), (5,6)) + self.assertEqual(keyboard.key_to_scan_codes('SHIFT'), (5,6)) + self.assertEqual(keyboard.key_to_scan_codes('ctrl'), keyboard.key_to_scan_codes('CONTROL')) + def test_key_to_scan_code_from_sided_modifier(self): + self.assertEqual(keyboard.key_to_scan_codes('left shift'), (5,)) + self.assertEqual(keyboard.key_to_scan_codes('right shift'), (6,)) + def test_key_to_scan_code_underscores(self): + self.assertEqual(keyboard.key_to_scan_codes('_'), (12,)) + self.assertEqual(keyboard.key_to_scan_codes('right_shift'), (6,)) + def test_key_to_scan_code_error_none(self): + with self.assertRaises(ValueError): + keyboard.key_to_scan_codes(None) + def test_key_to_scan_code_error_empty(self): + with self.assertRaises(ValueError): + keyboard.key_to_scan_codes('') + def test_key_to_scan_code_error_other(self): + with self.assertRaises(ValueError): + keyboard.key_to_scan_codes({}) + def test_key_to_scan_code_list(self): + self.assertEqual(keyboard.key_to_scan_codes([10, 5, 'a']), (10, 5, 1)) + def test_key_to_scan_code_empty(self): + with self.assertRaises(ValueError): + keyboard.key_to_scan_codes('none') + def test_key_to_scan_code_duplicated(self): + self.assertEqual(keyboard.key_to_scan_codes('duplicated'), (20,)) + + def test_parse_hotkey_simple(self): + self.assertEqual(keyboard.parse_hotkey('a'), (((1,),),)) + self.assertEqual(keyboard.parse_hotkey('A'), (((1,-1),),)) + def test_parse_hotkey_separators(self): + self.assertEqual(keyboard.parse_hotkey('+'), keyboard.parse_hotkey('plus')) + self.assertEqual(keyboard.parse_hotkey(','), keyboard.parse_hotkey('comma')) + def test_parse_hotkey_keys(self): + self.assertEqual(keyboard.parse_hotkey('left shift + a'), (((5,), (1,),),)) + self.assertEqual(keyboard.parse_hotkey('left shift+a'), (((5,), (1,),),)) + def test_parse_hotkey_simple_steps(self): + self.assertEqual(keyboard.parse_hotkey('a,b'), (((1,),),((2,),))) + self.assertEqual(keyboard.parse_hotkey('a, b'), (((1,),),((2,),))) + def test_parse_hotkey_steps(self): + self.assertEqual(keyboard.parse_hotkey('a+b, b+c'), (((1,),(2,)),((2,),(3,)))) + def test_parse_hotkey_example(self): + alt_codes = keyboard.key_to_scan_codes('alt') + shift_codes = keyboard.key_to_scan_codes('shift') + a_codes = keyboard.key_to_scan_codes('a') + b_codes = keyboard.key_to_scan_codes('b') + c_codes = keyboard.key_to_scan_codes('c') + self.assertEqual(keyboard.parse_hotkey("alt+shift+a, alt+b, c"), ((alt_codes, shift_codes, a_codes), (alt_codes, b_codes), (c_codes,))) + def test_parse_hotkey_list_scan_codes(self): + self.assertEqual(keyboard.parse_hotkey([1, 2, 3]), (((1,), (2,), (3,)),)) + def test_parse_hotkey_deep_list_scan_codes(self): + result = keyboard.parse_hotkey('a') + self.assertEqual(keyboard.parse_hotkey(result), (((1,),),)) + def test_parse_hotkey_list_names(self): + self.assertEqual(keyboard.parse_hotkey(['a', 'b', 'c']), (((1,), (2,), (3,)),)) + + def test_is_pressed_none(self): + self.assertFalse(keyboard.is_pressed('a')) + def test_is_pressed_true(self): + self.do(d_a) + self.assertTrue(keyboard.is_pressed('a')) + def test_is_pressed_true_scan_code_true(self): + self.do(d_a) + self.assertTrue(keyboard.is_pressed(1)) + def test_is_pressed_true_scan_code_false(self): + self.do(d_a) + self.assertFalse(keyboard.is_pressed(2)) + def test_is_pressed_true_scan_code_invalid(self): + self.do(d_a) + self.assertFalse(keyboard.is_pressed(-1)) + def test_is_pressed_false(self): + self.do(d_a+u_a+d_b) + self.assertFalse(keyboard.is_pressed('a')) + self.assertTrue(keyboard.is_pressed('b')) + def test_is_pressed_hotkey_true(self): + self.do(d_shift+d_a) + self.assertTrue(keyboard.is_pressed('shift+a')) + def test_is_pressed_hotkey_false(self): + self.do(d_shift+d_a+u_a) + self.assertFalse(keyboard.is_pressed('shift+a')) + def test_is_pressed_multi_step_fail(self): + self.do(u_a+d_a) + with self.assertRaises(ValueError): + keyboard.is_pressed('a, b') + + def test_send_single_press_release(self): + keyboard.send('a', do_press=True, do_release=True) + self.do([], d_a+u_a) + def test_send_single_press(self): + keyboard.send('a', do_press=True, do_release=False) + self.do([], d_a) + def test_send_single_release(self): + keyboard.send('a', do_press=False, do_release=True) + self.do([], u_a) + def test_send_single_none(self): + keyboard.send('a', do_press=False, do_release=False) + self.do([], []) + def test_press(self): + keyboard.press('a') + self.do([], d_a) + def test_release(self): + keyboard.release('a') + self.do([], u_a) + def test_press_and_release(self): + keyboard.press_and_release('a') + self.do([], d_a+u_a) + + def test_send_modifier_press_release(self): + keyboard.send('ctrl+a', do_press=True, do_release=True) + self.do([], d_ctrl+d_a+u_a+u_ctrl) + def test_send_modifiers_release(self): + keyboard.send('ctrl+shift+a', do_press=False, do_release=True) + self.do([], u_a+u_shift+u_ctrl) + + def test_call_later(self): + triggered = [] + def fn(arg1, arg2): + assert arg1 == 1 and arg2 == 2 + triggered.append(True) + keyboard.call_later(fn, (1, 2), 0.01) + self.assertFalse(triggered) + time.sleep(0.05) + self.assertTrue(triggered) + + def test_hook_nonblocking(self): + self.i = 0 + def count(e): + self.assertEqual(e.name, 'a') + self.i += 1 + hook = keyboard.hook(count, suppress=False) + self.do(d_a+u_a, d_a+u_a) + self.assertEqual(self.i, 2) + keyboard.unhook(hook) + self.do(d_a+u_a, d_a+u_a) + self.assertEqual(self.i, 2) + keyboard.hook(count, suppress=False) + self.do(d_a+u_a, d_a+u_a) + self.assertEqual(self.i, 4) + keyboard.unhook_all() + self.do(d_a+u_a, d_a+u_a) + self.assertEqual(self.i, 4) + def test_hook_blocking(self): + self.i = 0 + def count(e): + self.assertIn(e.name, ['a', 'b']) + self.i += 1 + return e.name == 'b' + hook = keyboard.hook(count, suppress=True) + self.do(d_a+d_b, d_b) + self.assertEqual(self.i, 2) + keyboard.unhook(hook) + self.do(d_a+d_b, d_a+d_b) + self.assertEqual(self.i, 2) + keyboard.hook(count, suppress=True) + self.do(d_a+d_b, d_b) + self.assertEqual(self.i, 4) + keyboard.unhook_all() + self.do(d_a+d_b, d_a+d_b) + self.assertEqual(self.i, 4) + def test_on_press_nonblocking(self): + keyboard.on_press(lambda e: self.assertEqual(e.name, 'a') and self.assertEqual(e.event_type, KEY_DOWN)) + self.do(d_a+u_a) + def test_on_press_blocking(self): + keyboard.on_press(lambda e: e.scan_code == 1, suppress=True) + self.do([make_event(KEY_DOWN, 'A', -1)] + d_a, d_a) + def test_on_release(self): + keyboard.on_release(lambda e: self.assertEqual(e.name, 'a') and self.assertEqual(e.event_type, KEY_UP)) + self.do(d_a+u_a) + + def test_hook_key_invalid(self): + with self.assertRaises(ValueError): + keyboard.hook_key('invalid', lambda e: None) + def test_hook_key_nonblocking(self): + self.i = 0 + def count(event): + self.i += 1 + hook = keyboard.hook_key('A', count) + self.do(d_a) + self.assertEqual(self.i, 1) + self.do(u_a+d_b) + self.assertEqual(self.i, 2) + self.do([make_event(KEY_DOWN, 'A', -1)]) + self.assertEqual(self.i, 3) + keyboard.unhook_key(hook) + self.do(d_a) + self.assertEqual(self.i, 3) + def test_hook_key_blocking(self): + self.i = 0 + def count(event): + self.i += 1 + return event.scan_code == 1 + hook = keyboard.hook_key('A', count, suppress=True) + self.do(d_a, d_a) + self.assertEqual(self.i, 1) + self.do(u_a+d_b, u_a+d_b) + self.assertEqual(self.i, 2) + self.do([make_event(KEY_DOWN, 'A', -1)], []) + self.assertEqual(self.i, 3) + keyboard.unhook_key(hook) + self.do([make_event(KEY_DOWN, 'A', -1)], [make_event(KEY_DOWN, 'A', -1)]) + self.assertEqual(self.i, 3) + def test_on_press_key_nonblocking(self): + keyboard.on_press_key('A', lambda e: self.assertEqual(e.name, 'a') and self.assertEqual(e.event_type, KEY_DOWN)) + self.do(d_a+u_a+d_b+u_b) + def test_on_press_key_blocking(self): + keyboard.on_press_key('A', lambda e: e.scan_code == 1, suppress=True) + self.do([make_event(KEY_DOWN, 'A', -1)] + d_a, d_a) + def test_on_release_key(self): + keyboard.on_release_key('a', lambda e: self.assertEqual(e.name, 'a') and self.assertEqual(e.event_type, KEY_UP)) + self.do(d_a+u_a) + + def test_block_key(self): + blocked = keyboard.block_key('a') + self.do(d_a+d_b, d_b) + self.do([make_event(KEY_DOWN, 'A', -1)], [make_event(KEY_DOWN, 'A', -1)]) + keyboard.unblock_key(blocked) + self.do(d_a+d_b, d_a+d_b) + def test_block_key_ambiguous(self): + keyboard.block_key('A') + self.do(d_a+d_b, d_b) + self.do([make_event(KEY_DOWN, 'A', -1)], []) + + def test_remap_key_simple(self): + mapped = keyboard.remap_key('a', 'b') + self.do(d_a+d_c+u_a, d_b+d_c+u_b) + keyboard.unremap_key(mapped) + self.do(d_a+d_c+u_a, d_a+d_c+u_a) + def test_remap_key_ambiguous(self): + keyboard.remap_key('A', 'b') + self.do(d_a+d_b, d_b+d_b) + self.do([make_event(KEY_DOWN, 'A', -1)], d_b) + def test_remap_key_multiple(self): + mapped = keyboard.remap_key('a', 'shift+b') + self.do(d_a+d_c+u_a, d_shift+d_b+d_c+u_b+u_shift) + keyboard.unremap_key(mapped) + self.do(d_a+d_c+u_a, d_a+d_c+u_a) + + def test_stash_state(self): + self.do(d_a+d_shift) + self.assertEqual(sorted(keyboard.stash_state()), [1, 5]) + self.do([], u_a+u_shift) + def test_restore_state(self): + self.do(d_b) + keyboard.restore_state([1, 5]) + self.do([], u_b+d_a+d_shift) + def test_restore_modifieres(self): + self.do(d_b) + keyboard.restore_modifiers([1, 5]) + self.do([], u_b+d_shift) + + def test_write_simple(self): + keyboard.write('a', exact=False) + self.do([], d_a+u_a) + def test_write_multiple(self): + keyboard.write('ab', exact=False) + self.do([], d_a+u_a+d_b+u_b) + def test_write_modifiers(self): + keyboard.write('Ab', exact=False) + self.do([], d_shift+d_a+u_a+u_shift+d_b+u_b) + # restore_state_after has been removed after the introduction of `restore_modifiers`. + #def test_write_stash_not_restore(self): + # self.do(d_shift) + # keyboard.write('a', restore_state_after=False, exact=False) + # self.do([], u_shift+d_a+u_a) + def test_write_stash_restore(self): + self.do(d_shift) + keyboard.write('a', exact=False) + self.do([], u_shift+d_a+u_a+d_shift) + def test_write_multiple(self): + last_time = time.time() + keyboard.write('ab', delay=0.01, exact=False) + self.do([], d_a+u_a+d_b+u_b) + self.assertGreater(time.time() - last_time, 0.015) + def test_write_unicode_explicit(self): + keyboard.write('ab', exact=True) + self.do([], [KeyboardEvent(event_type=KEY_DOWN, scan_code=999, name='a'), KeyboardEvent(event_type=KEY_DOWN, scan_code=999, name='b')]) + def test_write_unicode_fallback(self): + keyboard.write(u'áb', exact=False) + self.do([], [KeyboardEvent(event_type=KEY_DOWN, scan_code=999, name=u'á')]+d_b+u_b) + + def test_start_stop_recording(self): + keyboard.start_recording() + self.do(d_a+u_a) + self.assertEqual(keyboard.stop_recording(), d_a+u_a) + def test_stop_recording_error(self): + with self.assertRaises(ValueError): + keyboard.stop_recording() + + def test_record(self): + queue = keyboard._queue.Queue() + def process(): + queue.put(keyboard.record('space', suppress=True)) + from threading import Thread + t = Thread(target=process) + t.daemon = True + t.start() + # 0.01s sleep failed once already. Better solutions? + time.sleep(0.01) + self.do(du_a+du_b+du_space, du_a+du_b) + self.assertEqual(queue.get(timeout=0.5), du_a+du_b+du_space) + + def test_play_nodelay(self): + keyboard.play(d_a+u_a, 0) + self.do([], d_a+u_a) + def test_play_stash(self): + self.do(d_ctrl) + keyboard.play(d_a+u_a, 0) + self.do([], u_ctrl+d_a+u_a+d_ctrl) + def test_play_delay(self): + last_time = time.time() + events = [make_event(KEY_DOWN, 'a', 1, 100), make_event(KEY_UP, 'a', 1, 100.01)] + keyboard.play(events, 1) + self.do([], d_a+u_a) + self.assertGreater(time.time() - last_time, 0.005) + + def test_get_typed_strings_simple(self): + events = du_a+du_b+du_backspace+d_shift+du_a+u_shift+du_space+du_ctrl+du_a + self.assertEqual(list(keyboard.get_typed_strings(events)), ['aA ', 'a']) + def test_get_typed_strings_backspace(self): + events = du_a+du_b+du_backspace + self.assertEqual(list(keyboard.get_typed_strings(events)), ['a']) + events = du_backspace+du_a+du_b + self.assertEqual(list(keyboard.get_typed_strings(events)), ['ab']) + def test_get_typed_strings_shift(self): + events = d_shift+du_a+du_b+u_shift+du_space+du_ctrl+du_a + self.assertEqual(list(keyboard.get_typed_strings(events)), ['AB ', 'a']) + def test_get_typed_strings_all(self): + events = du_a+du_b+du_backspace+d_shift+du_a+du_capslock+du_b+u_shift+du_space+du_ctrl+du_a + self.assertEqual(list(keyboard.get_typed_strings(events)), ['aAb ', 'A']) + + def test_get_hotkey_name_simple(self): + self.assertEqual(keyboard.get_hotkey_name(['a']), 'a') + def test_get_hotkey_name_modifiers(self): + self.assertEqual(keyboard.get_hotkey_name(['a', 'shift', 'ctrl']), 'ctrl+shift+a') + def test_get_hotkey_name_normalize(self): + self.assertEqual(keyboard.get_hotkey_name(['SHIFT', 'left ctrl']), 'ctrl+shift') + def test_get_hotkey_name_plus(self): + self.assertEqual(keyboard.get_hotkey_name(['+']), 'plus') + def test_get_hotkey_name_duplicated(self): + self.assertEqual(keyboard.get_hotkey_name(['+', 'plus']), 'plus') + def test_get_hotkey_name_full(self): + self.assertEqual(keyboard.get_hotkey_name(['+', 'left ctrl', 'shift', 'WIN', 'right alt']), 'ctrl+alt+shift+windows+plus') + def test_get_hotkey_name_multiple(self): + self.assertEqual(keyboard.get_hotkey_name(['ctrl', 'b', '!', 'a']), 'ctrl+!+a+b') + def test_get_hotkey_name_from_pressed(self): + self.do(du_c+d_ctrl+d_a+d_b) + self.assertEqual(keyboard.get_hotkey_name(), 'ctrl+a+b') + + def test_read_hotkey(self): + queue = keyboard._queue.Queue() + def process(): + queue.put(keyboard.read_hotkey()) + from threading import Thread + t = Thread(target=process) + t.daemon = True + t.start() + time.sleep(0.01) + self.do(d_ctrl+d_a+d_b+u_ctrl) + self.assertEqual(queue.get(timeout=0.5), 'ctrl+a+b') + + def test_read_event(self): + queue = keyboard._queue.Queue() + def process(): + queue.put(keyboard.read_event(suppress=True)) + from threading import Thread + t = Thread(target=process) + t.daemon = True + t.start() + time.sleep(0.01) + self.do(d_a, []) + self.assertEqual(queue.get(timeout=0.5), d_a[0]) + + def test_read_key(self): + queue = keyboard._queue.Queue() + def process(): + queue.put(keyboard.read_key(suppress=True)) + from threading import Thread + t = Thread(target=process) + t.daemon = True + t.start() + time.sleep(0.01) + self.do(d_a, []) + self.assertEqual(queue.get(timeout=0.5), 'a') + + def test_wait_infinite(self): + self.triggered = False + def process(): + keyboard.wait() + self.triggered = True + from threading import Thread + t = Thread(target=process) + t.daemon = True # Yep, we are letting this thread loose. + t.start() + time.sleep(0.01) + self.assertFalse(self.triggered) + + def test_wait_until_success(self): + queue = keyboard._queue.Queue() + def process(): + queue.put(keyboard.wait(queue.get(timeout=0.5), suppress=True) or True) + from threading import Thread + t = Thread(target=process) + t.daemon = True + t.start() + queue.put('a') + time.sleep(0.01) + self.do(d_a, []) + self.assertTrue(queue.get(timeout=0.5)) + def test_wait_until_fail(self): + def process(): + keyboard.wait('a', suppress=True) + self.fail() + from threading import Thread + t = Thread(target=process) + t.daemon = True # Yep, we are letting this thread loose. + t.start() + time.sleep(0.01) + self.do(d_b) + + def test_add_hotkey_single_step_suppress_allow(self): + keyboard.add_hotkey('a', lambda: trigger() or True, suppress=True) + self.do(d_a, triggered_event+d_a) + def test_add_hotkey_single_step_suppress_args_allow(self): + arg = object() + keyboard.add_hotkey('a', lambda a: self.assertIs(a, arg) or trigger() or True, args=(arg,), suppress=True) + self.do(d_a, triggered_event+d_a) + def test_add_hotkey_single_step_suppress_single(self): + keyboard.add_hotkey('a', trigger, suppress=True) + self.do(d_a, triggered_event) + def test_add_hotkey_single_step_suppress_removed(self): + keyboard.remove_hotkey(keyboard.add_hotkey('a', trigger, suppress=True)) + self.do(d_a, d_a) + def test_add_hotkey_single_step_suppress_removed(self): + keyboard.remove_hotkey(keyboard.add_hotkey('ctrl+a', trigger, suppress=True)) + self.do(d_ctrl+d_a, d_ctrl+d_a) + self.assertEqual(keyboard._listener.filtered_modifiers[dummy_keys['left ctrl'][0][0]], 0) + def test_remove_hotkey_internal(self): + remove = keyboard.add_hotkey('shift+a', trigger, suppress=True) + self.assertTrue(all(keyboard._listener.blocking_hotkeys.values())) + self.assertTrue(all(keyboard._listener.filtered_modifiers.values())) + self.assertNotEqual(keyboard._hotkeys, {}) + remove() + self.assertTrue(not any(keyboard._listener.filtered_modifiers.values())) + self.assertTrue(not any(keyboard._listener.blocking_hotkeys.values())) + self.assertEqual(keyboard._hotkeys, {}) + def test_remove_hotkey_internal_multistep_start(self): + remove = keyboard.add_hotkey('shift+a, b', trigger, suppress=True) + self.assertTrue(all(keyboard._listener.blocking_hotkeys.values())) + self.assertTrue(all(keyboard._listener.filtered_modifiers.values())) + self.assertNotEqual(keyboard._hotkeys, {}) + remove() + self.assertTrue(not any(keyboard._listener.filtered_modifiers.values())) + self.assertTrue(not any(keyboard._listener.blocking_hotkeys.values())) + self.assertEqual(keyboard._hotkeys, {}) + def test_remove_hotkey_internal_multistep_end(self): + remove = keyboard.add_hotkey('shift+a, b', trigger, suppress=True) + self.do(d_shift+du_a+u_shift) + self.assertTrue(any(keyboard._listener.blocking_hotkeys.values())) + self.assertTrue(not any(keyboard._listener.filtered_modifiers.values())) + self.assertNotEqual(keyboard._hotkeys, {}) + remove() + self.assertTrue(not any(keyboard._listener.filtered_modifiers.values())) + self.assertTrue(not any(keyboard._listener.blocking_hotkeys.values())) + self.assertEqual(keyboard._hotkeys, {}) + def test_add_hotkey_single_step_suppress_with_modifiers(self): + keyboard.add_hotkey('ctrl+shift+a', trigger, suppress=True) + self.do(d_ctrl+d_shift+d_a, triggered_event) + def test_add_hotkey_single_step_suppress_with_modifiers_fail_unrelated_modifier(self): + keyboard.add_hotkey('ctrl+shift+a', trigger, suppress=True) + self.do(d_ctrl+d_shift+u_shift+d_a, d_shift+u_shift+d_ctrl+d_a) + def test_add_hotkey_single_step_suppress_with_modifiers_fail_unrelated_key(self): + keyboard.add_hotkey('ctrl+shift+a', trigger, suppress=True) + self.do(d_ctrl+d_shift+du_b, d_shift+d_ctrl+du_b) + def test_add_hotkey_single_step_suppress_with_modifiers_unrelated_key(self): + keyboard.add_hotkey('ctrl+shift+a', trigger, suppress=True) + self.do(d_ctrl+d_shift+du_b+d_a, d_shift+d_ctrl+du_b+triggered_event) + def test_add_hotkey_single_step_suppress_with_modifiers_release(self): + keyboard.add_hotkey('ctrl+shift+a', trigger, suppress=True) + self.do(d_ctrl+d_shift+du_b+d_a+u_ctrl+u_shift, d_shift+d_ctrl+du_b+triggered_event+u_ctrl+u_shift) + def test_add_hotkey_single_step_suppress_with_modifiers_out_of_order(self): + keyboard.add_hotkey('ctrl+shift+a', trigger, suppress=True) + self.do(d_shift+d_ctrl+d_a, triggered_event) + def test_add_hotkey_single_step_suppress_with_modifiers_repeated(self): + keyboard.add_hotkey('ctrl+a', trigger, suppress=True) + self.do(d_ctrl+du_a+du_b+du_a, triggered_event+d_ctrl+du_b+triggered_event) + def test_add_hotkey_single_step_suppress_with_modifiers_release(self): + keyboard.add_hotkey('ctrl+a', trigger, suppress=True, trigger_on_release=True) + self.do(d_ctrl+du_a+du_b+du_a, triggered_event+d_ctrl+du_b+triggered_event) + def test_add_hotkey_single_step_suppress_with_modifier_superset_release(self): + keyboard.add_hotkey('ctrl+a', trigger, suppress=True, trigger_on_release=True) + self.do(d_ctrl+d_shift+du_a+u_shift+u_ctrl, d_ctrl+d_shift+du_a+u_shift+u_ctrl) + def test_add_hotkey_single_step_suppress_with_modifier_superset(self): + keyboard.add_hotkey('ctrl+a', trigger, suppress=True) + self.do(d_ctrl+d_shift+du_a+u_shift+u_ctrl, d_ctrl+d_shift+du_a+u_shift+u_ctrl) + def test_add_hotkey_single_step_timeout(self): + keyboard.add_hotkey('a', trigger, timeout=1, suppress=True) + self.do(du_a, triggered_event) + def test_add_hotkey_multi_step_first_timeout(self): + keyboard.add_hotkey('a, b', trigger, timeout=0.01, suppress=True) + time.sleep(0.03) + self.do(du_a+du_b, triggered_event) + def test_add_hotkey_multi_step_last_timeout(self): + keyboard.add_hotkey('a, b', trigger, timeout=0.01, suppress=True) + self.do(du_a, []) + time.sleep(0.05) + self.do(du_b, du_a+du_b) + def test_add_hotkey_multi_step_success_timeout(self): + keyboard.add_hotkey('a, b', trigger, timeout=0.05, suppress=True) + self.do(du_a, []) + time.sleep(0.01) + self.do(du_b, triggered_event) + def test_add_hotkey_multi_step_suffix_timeout(self): + keyboard.add_hotkey('a, b, a', trigger, timeout=0.01, suppress=True) + self.do(du_a+du_b, []) + time.sleep(0.05) + self.do(du_a, du_a+du_b) + self.do(du_b+du_a, triggered_event) + def test_add_hotkey_multi_step_allow(self): + keyboard.add_hotkey('a, b', lambda: trigger() or True, suppress=True) + self.do(du_a+du_b, triggered_event+du_a+du_b) + + def test_add_hotkey_single_step_nonsuppress(self): + queue = keyboard._queue.Queue() + keyboard.add_hotkey('ctrl+shift+a+b', lambda: queue.put(True), suppress=False) + self.do(d_shift+d_ctrl+d_a+d_b) + self.assertTrue(queue.get(timeout=0.5)) + def test_add_hotkey_single_step_nonsuppress_repeated(self): + queue = keyboard._queue.Queue() + keyboard.add_hotkey('ctrl+shift+a+b', lambda: queue.put(True), suppress=False) + self.do(d_shift+d_ctrl+d_a+d_b) + self.do(d_shift+d_ctrl+d_a+d_b) + self.assertTrue(queue.get(timeout=0.5)) + self.assertTrue(queue.get(timeout=0.5)) + def test_add_hotkey_single_step_nosuppress_with_modifiers_out_of_order(self): + queue = keyboard._queue.Queue() + keyboard.add_hotkey('ctrl+shift+a', lambda: queue.put(True), suppress=False) + self.do(d_shift+d_ctrl+d_a) + self.assertTrue(queue.get(timeout=0.5)) + def test_add_hotkey_single_step_suppress_regression_1(self): + keyboard.add_hotkey('a', trigger, suppress=True) + self.do(d_c+d_a+u_c+u_a, d_c+d_a+u_c+u_a) + + def test_remap_hotkey_single(self): + keyboard.remap_hotkey('a', 'b') + self.do(d_a+u_a, d_b+u_b) + def test_remap_hotkey_complex_dst(self): + keyboard.remap_hotkey('a', 'ctrl+b, c') + self.do(d_a+u_a, d_ctrl+du_b+u_ctrl+du_c) + def test_remap_hotkey_modifiers(self): + keyboard.remap_hotkey('ctrl+shift+a', 'b') + self.do(d_ctrl+d_shift+d_a+u_a, du_b) + def test_remap_hotkey_modifiers_repeat(self): + keyboard.remap_hotkey('ctrl+shift+a', 'b') + self.do(d_ctrl+d_shift+du_a+du_a, du_b+du_b) + def test_remap_hotkey_modifiers_state(self): + keyboard.remap_hotkey('ctrl+shift+a', 'b') + self.do(d_ctrl+d_shift+du_c+du_a+du_a, d_shift+d_ctrl+du_c+u_shift+u_ctrl+du_b+d_ctrl+d_shift+u_shift+u_ctrl+du_b+d_ctrl+d_shift) + def test_remap_hotkey_release_incomplete(self): + keyboard.remap_hotkey('a', 'b', trigger_on_release=True) + self.do(d_a, []) + def test_remap_hotkey_release_complete(self): + keyboard.remap_hotkey('a', 'b', trigger_on_release=True) + self.do(du_a, du_b) + + def test_parse_hotkey_combinations_scan_code(self): + self.assertEqual(keyboard.parse_hotkey_combinations(30), (((30,),),)) + def test_parse_hotkey_combinations_single(self): + self.assertEqual(keyboard.parse_hotkey_combinations('a'), (((1,),),)) + def test_parse_hotkey_combinations_single_modifier(self): + self.assertEqual(keyboard.parse_hotkey_combinations('shift+a'), (((1, 5), (1, 6)),)) + def test_parse_hotkey_combinations_single_modifiers(self): + self.assertEqual(keyboard.parse_hotkey_combinations('shift+ctrl+a'), (((1, 5, 7), (1, 6, 7)),)) + def test_parse_hotkey_combinations_multi(self): + self.assertEqual(keyboard.parse_hotkey_combinations('a, b'), (((1,),), ((2,),))) + def test_parse_hotkey_combinations_multi_modifier(self): + self.assertEqual(keyboard.parse_hotkey_combinations('shift+a, b'), (((1, 5), (1, 6)), ((2,),))) + def test_parse_hotkey_combinations_list_list(self): + self.assertEqual(keyboard.parse_hotkey_combinations(keyboard.parse_hotkey_combinations('a, b')), keyboard.parse_hotkey_combinations('a, b')) + def test_parse_hotkey_combinations_fail_empty(self): + with self.assertRaises(ValueError): + keyboard.parse_hotkey_combinations('') + + + def test_add_hotkey_multistep_suppress_incomplete(self): + keyboard.add_hotkey('a, b', trigger, suppress=True) + self.do(du_a, []) + self.assertEqual(keyboard._listener.blocking_hotkeys[(1,)], []) + self.assertEqual(len(keyboard._listener.blocking_hotkeys[(2,)]), 1) + def test_add_hotkey_multistep_suppress_incomplete(self): + keyboard.add_hotkey('a, b', trigger, suppress=True) + self.do(du_a+du_b, triggered_event) + def test_add_hotkey_multistep_suppress_modifier(self): + keyboard.add_hotkey('shift+a, b', trigger, suppress=True) + self.do(d_shift+du_a+u_shift+du_b, triggered_event) + def test_add_hotkey_multistep_suppress_fail(self): + keyboard.add_hotkey('a, b', trigger, suppress=True) + self.do(du_a+du_c, du_a+du_c) + def test_add_hotkey_multistep_suppress_three_steps(self): + keyboard.add_hotkey('a, b, c', trigger, suppress=True) + self.do(du_a+du_b+du_c, triggered_event) + def test_add_hotkey_multistep_suppress_repeated_prefix(self): + keyboard.add_hotkey('a, a, c', trigger, suppress=True, trigger_on_release=True) + self.do(du_a+du_a+du_c, triggered_event) + def test_add_hotkey_multistep_suppress_repeated_key(self): + keyboard.add_hotkey('a, b', trigger, suppress=True) + self.do(du_a+du_a+du_b, du_a+triggered_event) + self.assertEqual(keyboard._listener.blocking_hotkeys[(2,)], []) + self.assertEqual(len(keyboard._listener.blocking_hotkeys[(1,)]), 1) + def test_add_hotkey_multi_step_suppress_regression_1(self): + keyboard.add_hotkey('a, b', trigger, suppress=True) + self.do(d_c+d_a+u_c+u_a+du_c, d_c+d_a+u_c+u_a+du_c) + def test_add_hotkey_multi_step_suppress_replays(self): + keyboard.add_hotkey('a, b, c', trigger, suppress=True) + self.do(du_a+du_b+du_a+du_b+du_space, du_a+du_b+du_a+du_b+du_space) + + def test_add_word_listener_success(self): + queue = keyboard._queue.Queue() + def free(): + queue.put(1) + keyboard.add_word_listener('abc', free) + self.do(du_a+du_b+du_c+du_space) + self.assertTrue(queue.get(timeout=0.5)) + def test_add_word_listener_no_trigger_fail(self): + queue = keyboard._queue.Queue() + def free(): + queue.put(1) + keyboard.add_word_listener('abc', free) + self.do(du_a+du_b+du_c) + with self.assertRaises(keyboard._queue.Empty): + queue.get(timeout=0.01) + def test_add_word_listener_timeout_fail(self): + queue = keyboard._queue.Queue() + def free(): + queue.put(1) + keyboard.add_word_listener('abc', free, timeout=1) + self.do(du_a+du_b+du_c+[make_event(KEY_DOWN, name='space', time=2)]) + with self.assertRaises(keyboard._queue.Empty): + queue.get(timeout=0.01) + def test_duplicated_word_listener(self): + keyboard.add_word_listener('abc', trigger) + keyboard.add_word_listener('abc', trigger) + def test_add_word_listener_remove(self): + queue = keyboard._queue.Queue() + def free(): + queue.put(1) + keyboard.add_word_listener('abc', free) + keyboard.remove_word_listener('abc') + self.do(du_a+du_b+du_c+du_space) + with self.assertRaises(keyboard._queue.Empty): + queue.get(timeout=0.01) + def test_add_word_listener_suffix_success(self): + queue = keyboard._queue.Queue() + def free(): + queue.put(1) + keyboard.add_word_listener('abc', free, match_suffix=True) + self.do(du_a+du_a+du_b+du_c+du_space) + self.assertTrue(queue.get(timeout=0.5)) + def test_add_word_listener_suffix_fail(self): + queue = keyboard._queue.Queue() + def free(): + queue.put(1) + keyboard.add_word_listener('abc', free) + self.do(du_a+du_a+du_b+du_c) + with self.assertRaises(keyboard._queue.Empty): + queue.get(timeout=0.01) + + #def test_add_abbreviation(self): + # keyboard.add_abbreviation('abc', 'aaa') + # self.do(du_a+du_b+du_c+du_space, []) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/keyboard/_mouse_event.py b/keyboard/_mouse_event.py new file mode 100644 index 0000000..38b8961 --- /dev/null +++ b/keyboard/_mouse_event.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from collections import namedtuple + +LEFT = 'left' +RIGHT = 'right' +MIDDLE = 'middle' +WHEEL = 'wheel' +X = 'x' +X2 = 'x2' + +UP = 'up' +DOWN = 'down' +DOUBLE = 'double' +VERTICAL = 'vertical' +HORIZONTAL = 'horizontal' + + +ButtonEvent = namedtuple('ButtonEvent', ['event_type', 'button', 'time']) +WheelEvent = namedtuple('WheelEvent', ['delta', 'time']) +MoveEvent = namedtuple('MoveEvent', ['x', 'y', 'time']) diff --git a/keyboard/_mouse_tests.py b/keyboard/_mouse_tests.py new file mode 100644 index 0000000..6a2b2e4 --- /dev/null +++ b/keyboard/_mouse_tests.py @@ -0,0 +1,271 @@ +# -*- coding: utf-8 -*- +import unittest +import time + +from ._mouse_event import MoveEvent, ButtonEvent, WheelEvent, LEFT, RIGHT, MIDDLE, X, X2, UP, DOWN, DOUBLE +from keyboard import mouse + +class FakeOsMouse(object): + def __init__(self): + self.append = None + self.position = (0, 0) + self.queue = None + self.init = lambda: None + + def listen(self, queue): + self.listening = True + self.queue = queue + + def press(self, button): + self.append((DOWN, button)) + + def release(self, button): + self.append((UP, button)) + + def get_position(self): + return self.position + + def move_to(self, x, y): + self.append(('move', (x, y))) + self.position = (x, y) + + def wheel(self, delta): + self.append(('wheel', delta)) + + def move_relative(self, x, y): + self.position = (self.position[0] + x, self.position[1] + y) + +class TestMouse(unittest.TestCase): + @staticmethod + def setUpClass(): + mouse._os_mouse= FakeOsMouse() + mouse._listener.start_if_necessary() + assert mouse._os_mouse.listening + + def setUp(self): + self.events = [] + mouse._pressed_events.clear() + mouse._os_mouse.append = self.events.append + + def tearDown(self): + mouse.unhook_all() + # Make sure there's no spill over between tests. + self.wait_for_events_queue() + + def wait_for_events_queue(self): + mouse._listener.queue.join() + + def flush_events(self): + self.wait_for_events_queue() + events = list(self.events) + # Ugly, but requried to work in Python2. Python3 has list.clear + del self.events[:] + return events + + def press(self, button=LEFT): + mouse._os_mouse.queue.put(ButtonEvent(DOWN, button, time.time())) + self.wait_for_events_queue() + + def release(self, button=LEFT): + mouse._os_mouse.queue.put(ButtonEvent(UP, button, time.time())) + self.wait_for_events_queue() + + def double_click(self, button=LEFT): + mouse._os_mouse.queue.put(ButtonEvent(DOUBLE, button, time.time())) + self.wait_for_events_queue() + + def click(self, button=LEFT): + self.press(button) + self.release(button) + + def wheel(self, delta=1): + mouse._os_mouse.queue.put(WheelEvent(delta, time.time())) + self.wait_for_events_queue() + + def move(self, x=0, y=0): + mouse._os_mouse.queue.put(MoveEvent(x, y, time.time())) + self.wait_for_events_queue() + + def test_hook(self): + events = [] + self.press() + mouse.hook(events.append) + self.press() + mouse.unhook(events.append) + self.press() + self.assertEqual(len(events), 1) + + def test_is_pressed(self): + self.assertFalse(mouse.is_pressed()) + self.press() + self.assertTrue(mouse.is_pressed()) + self.release() + self.press(X2) + self.assertFalse(mouse.is_pressed()) + + self.assertTrue(mouse.is_pressed(X2)) + self.press(X2) + self.assertTrue(mouse.is_pressed(X2)) + self.release(X2) + self.release(X2) + self.assertFalse(mouse.is_pressed(X2)) + + def test_buttons(self): + mouse.press() + self.assertEqual(self.flush_events(), [(DOWN, LEFT)]) + mouse.release() + self.assertEqual(self.flush_events(), [(UP, LEFT)]) + mouse.click() + self.assertEqual(self.flush_events(), [(DOWN, LEFT), (UP, LEFT)]) + mouse.double_click() + self.assertEqual(self.flush_events(), [(DOWN, LEFT), (UP, LEFT), (DOWN, LEFT), (UP, LEFT)]) + mouse.right_click() + self.assertEqual(self.flush_events(), [(DOWN, RIGHT), (UP, RIGHT)]) + mouse.click(RIGHT) + self.assertEqual(self.flush_events(), [(DOWN, RIGHT), (UP, RIGHT)]) + mouse.press(X2) + self.assertEqual(self.flush_events(), [(DOWN, X2)]) + + def test_position(self): + self.assertEqual(mouse.get_position(), mouse._os_mouse.get_position()) + + def test_move(self): + mouse.move(0, 0) + self.assertEqual(mouse._os_mouse.get_position(), (0, 0)) + mouse.move(100, 500) + self.assertEqual(mouse._os_mouse.get_position(), (100, 500)) + mouse.move(1, 2, False) + self.assertEqual(mouse._os_mouse.get_position(), (101, 502)) + + mouse.move(0, 0) + mouse.move(100, 499, True, duration=0.01) + self.assertEqual(mouse._os_mouse.get_position(), (100, 499)) + mouse.move(100, 1, False, duration=0.01) + self.assertEqual(mouse._os_mouse.get_position(), (200, 500)) + mouse.move(0, 0, False, duration=0.01) + self.assertEqual(mouse._os_mouse.get_position(), (200, 500)) + + def triggers(self, fn, events, **kwargs): + self.triggered = False + def callback(): + self.triggered = True + handler = fn(callback, **kwargs) + + for event_type, arg in events: + if event_type == DOWN: + self.press(arg) + elif event_type == UP: + self.release(arg) + elif event_type == DOUBLE: + self.double_click(arg) + elif event_type == 'WHEEL': + self.wheel() + + mouse._listener.remove_handler(handler) + return self.triggered + + def test_on_button(self): + self.assertTrue(self.triggers(mouse.on_button, [(DOWN, LEFT)])) + self.assertTrue(self.triggers(mouse.on_button, [(DOWN, RIGHT)])) + self.assertTrue(self.triggers(mouse.on_button, [(DOWN, X)])) + + self.assertFalse(self.triggers(mouse.on_button, [('WHEEL', '')])) + + self.assertFalse(self.triggers(mouse.on_button, [(DOWN, X)], buttons=MIDDLE)) + self.assertTrue(self.triggers(mouse.on_button, [(DOWN, MIDDLE)], buttons=MIDDLE)) + self.assertTrue(self.triggers(mouse.on_button, [(DOWN, MIDDLE)], buttons=MIDDLE)) + self.assertFalse(self.triggers(mouse.on_button, [(DOWN, MIDDLE)], buttons=MIDDLE, types=UP)) + self.assertTrue(self.triggers(mouse.on_button, [(UP, MIDDLE)], buttons=MIDDLE, types=UP)) + + self.assertTrue(self.triggers(mouse.on_button, [(UP, MIDDLE)], buttons=[MIDDLE, LEFT], types=[UP, DOWN])) + self.assertTrue(self.triggers(mouse.on_button, [(DOWN, LEFT)], buttons=[MIDDLE, LEFT], types=[UP, DOWN])) + self.assertFalse(self.triggers(mouse.on_button, [(UP, X)], buttons=[MIDDLE, LEFT], types=[UP, DOWN])) + + def test_ons(self): + self.assertTrue(self.triggers(mouse.on_click, [(UP, LEFT)])) + self.assertFalse(self.triggers(mouse.on_click, [(UP, RIGHT)])) + self.assertFalse(self.triggers(mouse.on_click, [(DOWN, LEFT)])) + self.assertFalse(self.triggers(mouse.on_click, [(DOWN, RIGHT)])) + + self.assertTrue(self.triggers(mouse.on_double_click, [(DOUBLE, LEFT)])) + self.assertFalse(self.triggers(mouse.on_double_click, [(DOUBLE, RIGHT)])) + self.assertFalse(self.triggers(mouse.on_double_click, [(DOWN, RIGHT)])) + + self.assertTrue(self.triggers(mouse.on_right_click, [(UP, RIGHT)])) + self.assertTrue(self.triggers(mouse.on_middle_click, [(UP, MIDDLE)])) + + def test_wait(self): + # If this fails it blocks. Unfortunately, but I see no other way of testing. + from threading import Thread, Lock + lock = Lock() + lock.acquire() + def t(): + mouse.wait() + lock.release() + Thread(target=t).start() + self.press() + lock.acquire() + + def test_record_play(self): + from threading import Thread, Lock + lock = Lock() + lock.acquire() + def t(): + self.recorded = mouse.record(RIGHT) + lock.release() + Thread(target=t).start() + self.click() + self.wheel(5) + self.move(100, 50) + self.press(RIGHT) + lock.acquire() + + self.assertEqual(len(self.recorded), 5) + self.assertEqual(self.recorded[0]._replace(time=None), ButtonEvent(DOWN, LEFT, None)) + self.assertEqual(self.recorded[1]._replace(time=None), ButtonEvent(UP, LEFT, None)) + self.assertEqual(self.recorded[2]._replace(time=None), WheelEvent(5, None)) + self.assertEqual(self.recorded[3]._replace(time=None), MoveEvent(100, 50, None)) + self.assertEqual(self.recorded[4]._replace(time=None), ButtonEvent(DOWN, RIGHT, None)) + + mouse.play(self.recorded, speed_factor=0) + events = self.flush_events() + self.assertEqual(len(events), 5) + self.assertEqual(events[0], (DOWN, LEFT)) + self.assertEqual(events[1], (UP, LEFT)) + self.assertEqual(events[2], ('wheel', 5)) + self.assertEqual(events[3], ('move', (100, 50))) + self.assertEqual(events[4], (DOWN, RIGHT)) + + mouse.play(self.recorded) + events = self.flush_events() + self.assertEqual(len(events), 5) + self.assertEqual(events[0], (DOWN, LEFT)) + self.assertEqual(events[1], (UP, LEFT)) + self.assertEqual(events[2], ('wheel', 5)) + self.assertEqual(events[3], ('move', (100, 50))) + self.assertEqual(events[4], (DOWN, RIGHT)) + + mouse.play(self.recorded, include_clicks=False) + events = self.flush_events() + self.assertEqual(len(events), 2) + self.assertEqual(events[0], ('wheel', 5)) + self.assertEqual(events[1], ('move', (100, 50))) + + mouse.play(self.recorded, include_moves=False) + events = self.flush_events() + self.assertEqual(len(events), 4) + self.assertEqual(events[0], (DOWN, LEFT)) + self.assertEqual(events[1], (UP, LEFT)) + self.assertEqual(events[2], ('wheel', 5)) + self.assertEqual(events[3], (DOWN, RIGHT)) + + mouse.play(self.recorded, include_wheel=False) + events = self.flush_events() + self.assertEqual(len(events), 4) + self.assertEqual(events[0], (DOWN, LEFT)) + self.assertEqual(events[1], (UP, LEFT)) + self.assertEqual(events[2], ('move', (100, 50))) + self.assertEqual(events[3], (DOWN, RIGHT)) + +if __name__ == '__main__': + unittest.main() diff --git a/keyboard/_nixcommon.py b/keyboard/_nixcommon.py new file mode 100644 index 0000000..a4d0d06 --- /dev/null +++ b/keyboard/_nixcommon.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +import struct +import os +import atexit +from time import time as now +from threading import Thread +from glob import glob +try: + from queue import Queue +except ImportError: + from Queue import Queue + +event_bin_format = 'llHHI' + +# Taken from include/linux/input.h +# https://www.kernel.org/doc/Documentation/input/event-codes.txt +EV_SYN = 0x00 +EV_KEY = 0x01 +EV_REL = 0x02 +EV_ABS = 0x03 +EV_MSC = 0x04 + +def make_uinput(): + if not os.path.exists('/dev/uinput'): + raise IOError('No uinput module found.') + + import fcntl, struct + + # Requires uinput driver, but it's usually available. + uinput = open("/dev/uinput", 'wb') + UI_SET_EVBIT = 0x40045564 + fcntl.ioctl(uinput, UI_SET_EVBIT, EV_KEY) + + UI_SET_KEYBIT = 0x40045565 + for i in range(256): + fcntl.ioctl(uinput, UI_SET_KEYBIT, i) + + BUS_USB = 0x03 + uinput_user_dev = "80sHHHHi64i64i64i64i" + axis = [0] * 64 * 4 + uinput.write(struct.pack(uinput_user_dev, b"Virtual Keyboard", BUS_USB, 1, 1, 1, 0, *axis)) + uinput.flush() # Without this you may get Errno 22: Invalid argument. + + UI_DEV_CREATE = 0x5501 + fcntl.ioctl(uinput, UI_DEV_CREATE) + UI_DEV_DESTROY = 0x5502 + #fcntl.ioctl(uinput, UI_DEV_DESTROY) + + return uinput + +class EventDevice(object): + def __init__(self, path): + self.path = path + self._input_file = None + self._output_file = None + + @property + def input_file(self): + if self._input_file is None: + try: + self._input_file = open(self.path, 'rb') + except IOError as e: + if e.strerror == 'Permission denied': + print('Permission denied ({}). You must be sudo to access global events.'.format(self.path)) + exit() + + def try_close(): + try: + self._input_file.close + except: + pass + atexit.register(try_close) + return self._input_file + + @property + def output_file(self): + if self._output_file is None: + self._output_file = open(self.path, 'wb') + atexit.register(self._output_file.close) + return self._output_file + + def read_event(self): + data = self.input_file.read(struct.calcsize(event_bin_format)) + seconds, microseconds, type, code, value = struct.unpack(event_bin_format, data) + return seconds + microseconds / 1e6, type, code, value, self.path + + def write_event(self, type, code, value): + integer, fraction = divmod(now(), 1) + seconds = int(integer) + microseconds = int(fraction * 1e6) + data_event = struct.pack(event_bin_format, seconds, microseconds, type, code, value) + + # Send a sync event to ensure other programs update. + sync_event = struct.pack(event_bin_format, seconds, microseconds, EV_SYN, 0, 0) + + self.output_file.write(data_event + sync_event) + self.output_file.flush() + +class AggregatedEventDevice(object): + def __init__(self, devices, output=None): + self.event_queue = Queue() + self.devices = devices + self.output = output or self.devices[0] + def start_reading(device): + while True: + self.event_queue.put(device.read_event()) + for device in self.devices: + thread = Thread(target=start_reading, args=[device]) + thread.setDaemon(True) + thread.start() + + def read_event(self): + return self.event_queue.get(block=True) + + def write_event(self, type, code, value): + self.output.write_event(type, code, value) + +import re +from collections import namedtuple +DeviceDescription = namedtuple('DeviceDescription', 'event_file is_mouse is_keyboard') +device_pattern = r"""N: Name="([^"]+?)".+?H: Handlers=([^\n]+)""" +def list_devices_from_proc(type_name): + try: + with open('/proc/bus/input/devices') as f: + description = f.read() + except FileNotFoundError: + return + + devices = {} + for name, handlers in re.findall(device_pattern, description, re.DOTALL): + path = '/dev/input/event' + re.search(r'event(\d+)', handlers).group(1) + if type_name in handlers: + yield EventDevice(path) + +def list_devices_from_by_id(name_suffix, by_id=True): + for path in glob('/dev/input/{}/*-event-{}'.format('by-id' if by_id else 'by-path', name_suffix)): + yield EventDevice(path) + +def aggregate_devices(type_name): + # Some systems have multiple keyboards with different range of allowed keys + # on each one, like a notebook with a "keyboard" device exclusive for the + # power button. Instead of figuring out which keyboard allows which key to + # send events, we create a fake device and send all events through there. + try: + uinput = make_uinput() + fake_device = EventDevice('uinput Fake Device') + fake_device._input_file = uinput + fake_device._output_file = uinput + except IOError as e: + import warnings + warnings.warn('Failed to create a device file using `uinput` module. Sending of events may be limited or unavailable depending on plugged-in devices.', stacklevel=2) + fake_device = None + + # We don't aggregate devices from different sources to avoid + # duplicates. + + devices_from_proc = list(list_devices_from_proc(type_name)) + if devices_from_proc: + return AggregatedEventDevice(devices_from_proc, output=fake_device) + + # breaks on mouse for virtualbox + # was getting /dev/input/by-id/usb-VirtualBox_USB_Tablet-event-mouse + devices_from_by_id = list(list_devices_from_by_id(type_name)) or list(list_devices_from_by_id(type_name, by_id=False)) + if devices_from_by_id: + return AggregatedEventDevice(devices_from_by_id, output=fake_device) + + # If no keyboards were found we can only use the fake device to send keys. + assert fake_device + return fake_device + + +def ensure_root(): + if os.geteuid() != 0: + raise ImportError('You must be root to use this library on linux.') diff --git a/keyboard/_nixkeyboard.py b/keyboard/_nixkeyboard.py new file mode 100644 index 0000000..d3950a1 --- /dev/null +++ b/keyboard/_nixkeyboard.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- +import struct +import traceback +from time import time as now +from collections import namedtuple +from ._keyboard_event import KeyboardEvent, KEY_DOWN, KEY_UP +from ._canonical_names import all_modifiers, normalize_name +from ._nixcommon import EV_KEY, aggregate_devices, ensure_root + +# TODO: start by reading current keyboard state, as to not missing any already pressed keys. +# See: http://stackoverflow.com/questions/3649874/how-to-get-keyboard-state-in-linux + +def cleanup_key(name): + """ Formats a dumpkeys format to our standard. """ + name = name.lstrip('+') + is_keypad = name.startswith('KP_') + for mod in ('Meta_', 'Control_', 'dead_', 'KP_'): + if name.startswith(mod): + name = name[len(mod):] + + # Dumpkeys is weird like that. + if name == 'Remove': + name = 'Delete' + elif name == 'Delete': + name = 'Backspace' + + if name.endswith('_r'): + name = 'right ' + name[:-2] + if name.endswith('_l'): + name = 'left ' + name[:-2] + + + return normalize_name(name), is_keypad + +def cleanup_modifier(modifier): + modifier = normalize_name(modifier) + if modifier in all_modifiers: + return modifier + if modifier[:-1] in all_modifiers: + return modifier[:-1] + raise ValueError('Unknown modifier {}'.format(modifier)) + +""" +Use `dumpkeys --keys-only` to list all scan codes and their names. We +then parse the output and built a table. For each scan code and modifiers we +have a list of names and vice-versa. +""" +from subprocess import check_output +from collections import defaultdict +import re + +to_name = defaultdict(list) +from_name = defaultdict(list) +keypad_scan_codes = set() + +def register_key(key_and_modifiers, name): + if name not in to_name[key_and_modifiers]: + to_name[key_and_modifiers].append(name) + if key_and_modifiers not in from_name[name]: + from_name[name].append(key_and_modifiers) + +def build_tables(): + if to_name and from_name: return + ensure_root() + + modifiers_bits = { + 'shift': 1, + 'alt gr': 2, + 'ctrl': 4, + 'alt': 8, + } + keycode_template = r'^keycode\s+(\d+)\s+=(.*?)$' + dump = check_output(['dumpkeys', '--keys-only'], universal_newlines=True) + for str_scan_code, str_names in re.findall(keycode_template, dump, re.MULTILINE): + scan_code = int(str_scan_code) + for i, str_name in enumerate(str_names.strip().split()): + modifiers = tuple(sorted(modifier for modifier, bit in modifiers_bits.items() if i & bit)) + name, is_keypad = cleanup_key(str_name) + register_key((scan_code, modifiers), name) + if is_keypad: + keypad_scan_codes.add(scan_code) + register_key((scan_code, modifiers), 'keypad ' + name) + + # dumpkeys consistently misreports the Windows key, sometimes + # skipping it completely or reporting as 'alt. 125 = left win, + # 126 = right win. + if (125, ()) not in to_name or to_name[(125, ())] == 'alt': + register_key((125, ()), 'windows') + if (126, ()) not in to_name or to_name[(126, ())] == 'alt': + register_key((126, ()), 'windows') + + # The menu key is usually skipped altogether, so we also add it manually. + if (127, ()) not in to_name: + register_key((127, ()), 'menu') + + synonyms_template = r'^(\S+)\s+for (.+)$' + dump = check_output(['dumpkeys', '--long-info'], universal_newlines=True) + for synonym_str, original_str in re.findall(synonyms_template, dump, re.MULTILINE): + synonym, _ = cleanup_key(synonym_str) + original, _ = cleanup_key(original_str) + if synonym != original: + from_name[original].extend(from_name[synonym]) + from_name[synonym].extend(from_name[original]) + +device = None +def build_device(): + global device + if device: return + ensure_root() + device = aggregate_devices('kbd') + +def init(): + build_device() + build_tables() + +pressed_modifiers = set() + +def listen(callback): + build_device() + build_tables() + + while True: + time, type, code, value, device_id = device.read_event() + if type != EV_KEY: + continue + + scan_code = code + event_type = KEY_DOWN if value else KEY_UP # 0 = UP, 1 = DOWN, 2 = HOLD + + pressed_modifiers_tuple = tuple(sorted(pressed_modifiers)) + names = to_name[(scan_code, pressed_modifiers_tuple)] or to_name[(scan_code, ())] or ['unknown'] + name = names[0] + + if name in all_modifiers: + if event_type == KEY_DOWN: + pressed_modifiers.add(name) + else: + pressed_modifiers.discard(name) + + is_keypad = scan_code in keypad_scan_codes + callback(KeyboardEvent(event_type=event_type, scan_code=scan_code, name=name, time=time, device=device_id, is_keypad=is_keypad, modifiers=pressed_modifiers_tuple)) + +def write_event(scan_code, is_down): + build_device() + device.write_event(EV_KEY, scan_code, int(is_down)) + +def map_name(name): + build_tables() + for entry in from_name[name]: + yield entry + + parts = name.split(' ', 1) + if len(parts) > 1 and parts[0] in ('left', 'right'): + for entry in from_name[parts[1]]: + yield entry + +def press(scan_code): + write_event(scan_code, True) + +def release(scan_code): + write_event(scan_code, False) + +def type_unicode(character): + codepoint = ord(character) + hexadecimal = hex(codepoint)[len('0x'):] + + for key in ['ctrl', 'shift', 'u']: + scan_code, _ = next(map_name(key)) + press(scan_code) + + for key in hexadecimal: + scan_code, _ = next(map_name(key)) + press(scan_code) + release(scan_code) + + for key in ['ctrl', 'shift', 'u']: + scan_code, _ = next(map_name(key)) + release(scan_code) + +if __name__ == '__main__': + def p(e): + print(e) + listen(p) diff --git a/keyboard/_nixmouse.py b/keyboard/_nixmouse.py new file mode 100644 index 0000000..6b02c57 --- /dev/null +++ b/keyboard/_nixmouse.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +import struct +from subprocess import check_output +import re +from ._nixcommon import EV_KEY, EV_REL, EV_MSC, EV_SYN, EV_ABS, aggregate_devices, ensure_root +from ._mouse_event import ButtonEvent, WheelEvent, MoveEvent, LEFT, RIGHT, MIDDLE, X, X2, UP, DOWN + +import ctypes +import ctypes.util +from ctypes import c_uint32, c_uint, c_int, byref + +display = None +window = None +x11 = None +def build_display(): + global display, window, x11 + if display and window and x11: return + x11 = ctypes.cdll.LoadLibrary(ctypes.util.find_library('X11')) + # Required because we will have multiple threads calling x11, + # such as the listener thread and then main using "move_to". + x11.XInitThreads() + display = x11.XOpenDisplay(None) + # Known to cause segfault in Fedora 23 64bits, no known workarounds. + # http://stackoverflow.com/questions/35137007/get-mouse-position-on-linux-pure-python + window = x11.XDefaultRootWindow(display) + +def get_position(): + build_display() + root_id, child_id = c_uint32(), c_uint32() + root_x, root_y, win_x, win_y = c_int(), c_int(), c_int(), c_int() + mask = c_uint() + ret = x11.XQueryPointer(display, c_uint32(window), byref(root_id), byref(child_id), + byref(root_x), byref(root_y), + byref(win_x), byref(win_y), byref(mask)) + return root_x.value, root_y.value + +def move_to(x, y): + build_display() + x11.XWarpPointer(display, None, window, 0, 0, 0, 0, x, y) + x11.XFlush(display) + +REL_X = 0x00 +REL_Y = 0x01 +REL_Z = 0x02 +REL_HWHEEL = 0x06 +REL_WHEEL = 0x08 + +ABS_X = 0x00 +ABS_Y = 0x01 + +BTN_MOUSE = 0x110 +BTN_LEFT = 0x110 +BTN_RIGHT = 0x111 +BTN_MIDDLE = 0x112 +BTN_SIDE = 0x113 +BTN_EXTRA = 0x114 + +button_by_code = { + BTN_LEFT: LEFT, + BTN_RIGHT: RIGHT, + BTN_MIDDLE: MIDDLE, + BTN_SIDE: X, + BTN_EXTRA: X2, +} +code_by_button = {button: code for code, button in button_by_code.items()} + +device = None +def build_device(): + global device + if device: return + ensure_root() + device = aggregate_devices('mouse') +init = build_device + +def listen(queue): + build_device() + + while True: + time, type, code, value, device_id = device.read_event() + if type == EV_SYN or type == EV_MSC: + continue + + event = None + arg = None + + if type == EV_KEY: + event = ButtonEvent(DOWN if value else UP, button_by_code.get(code, '?'), time) + elif type == EV_REL: + value, = struct.unpack('i', struct.pack('I', value)) + + if code == REL_WHEEL: + event = WheelEvent(value, time) + elif code in (REL_X, REL_Y): + x, y = get_position() + event = MoveEvent(x, y, time) + + if event is None: + # Unknown event type. + continue + + queue.put(event) + +def press(button=LEFT): + build_device() + device.write_event(EV_KEY, code_by_button[button], 0x01) + +def release(button=LEFT): + build_device() + device.write_event(EV_KEY, code_by_button[button], 0x00) + +def move_relative(x, y): + build_device() + # Note relative events are not in terms of pixels, but millimeters. + if x < 0: + x += 2**32 + if y < 0: + y += 2**32 + device.write_event(EV_REL, REL_X, x) + device.write_event(EV_REL, REL_Y, y) + +def wheel(delta=1): + build_device() + if delta < 0: + delta += 2**32 + device.write_event(EV_REL, REL_WHEEL, delta) + + +if __name__ == '__main__': + #listen(print) + move_to(100, 200) diff --git a/keyboard/_winkeyboard.py b/keyboard/_winkeyboard.py new file mode 100644 index 0000000..528e95f --- /dev/null +++ b/keyboard/_winkeyboard.py @@ -0,0 +1,620 @@ +# -*- coding: utf-8 -*- +""" +This is the Windows backend for keyboard events, and is implemented by +invoking the Win32 API through the ctypes module. This is error prone +and can introduce very unpythonic failure modes, such as segfaults and +low level memory leaks. But it is also dependency-free, very performant +well documented on Microsoft's webstie and scattered examples. + +# TODO: +- Keypad numbers still print as numbers even when numlock is off. +- No way to specify if user wants a keypad key or not in `map_char`. +""" +from __future__ import unicode_literals +import re +import atexit +import traceback +from threading import Lock +from collections import defaultdict + +from ._keyboard_event import KeyboardEvent, KEY_DOWN, KEY_UP +from ._canonical_names import normalize_name +try: + # Force Python2 to convert to unicode and not to str. + chr = unichr +except NameError: + pass + +# This part is just declaring Win32 API structures using ctypes. In C +# this would be simply #include "windows.h". + +import ctypes +from ctypes import c_short, c_char, c_uint8, c_int32, c_int, c_uint, c_uint32, c_long, Structure, CFUNCTYPE, POINTER +from ctypes.wintypes import WORD, DWORD, BOOL, HHOOK, MSG, LPWSTR, WCHAR, WPARAM, LPARAM, LONG, HMODULE, LPCWSTR, HINSTANCE, HWND +LPMSG = POINTER(MSG) +ULONG_PTR = POINTER(DWORD) + +kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) +GetModuleHandleW = kernel32.GetModuleHandleW +GetModuleHandleW.restype = HMODULE +GetModuleHandleW.argtypes = [LPCWSTR] + +#https://github.com/boppreh/mouse/issues/1 +#user32 = ctypes.windll.user32 +user32 = ctypes.WinDLL('user32', use_last_error = True) + +VK_PACKET = 0xE7 + +INPUT_MOUSE = 0 +INPUT_KEYBOARD = 1 +INPUT_HARDWARE = 2 + +KEYEVENTF_KEYUP = 0x02 +KEYEVENTF_UNICODE = 0x04 + +class KBDLLHOOKSTRUCT(Structure): + _fields_ = [("vk_code", DWORD), + ("scan_code", DWORD), + ("flags", DWORD), + ("time", c_int), + ("dwExtraInfo", ULONG_PTR)] + +# Included for completeness. +class MOUSEINPUT(ctypes.Structure): + _fields_ = (('dx', LONG), + ('dy', LONG), + ('mouseData', DWORD), + ('dwFlags', DWORD), + ('time', DWORD), + ('dwExtraInfo', ULONG_PTR)) + +class KEYBDINPUT(ctypes.Structure): + _fields_ = (('wVk', WORD), + ('wScan', WORD), + ('dwFlags', DWORD), + ('time', DWORD), + ('dwExtraInfo', ULONG_PTR)) + +class HARDWAREINPUT(ctypes.Structure): + _fields_ = (('uMsg', DWORD), + ('wParamL', WORD), + ('wParamH', WORD)) + +class _INPUTunion(ctypes.Union): + _fields_ = (('mi', MOUSEINPUT), + ('ki', KEYBDINPUT), + ('hi', HARDWAREINPUT)) + +class INPUT(ctypes.Structure): + _fields_ = (('type', DWORD), + ('union', _INPUTunion)) + +LowLevelKeyboardProc = CFUNCTYPE(c_int, WPARAM, LPARAM, POINTER(KBDLLHOOKSTRUCT)) + +SetWindowsHookEx = user32.SetWindowsHookExW +SetWindowsHookEx.argtypes = [c_int, LowLevelKeyboardProc, HINSTANCE , DWORD] +SetWindowsHookEx.restype = HHOOK + +CallNextHookEx = user32.CallNextHookEx +#CallNextHookEx.argtypes = [c_int , c_int, c_int, POINTER(KBDLLHOOKSTRUCT)] +CallNextHookEx.restype = c_int + +UnhookWindowsHookEx = user32.UnhookWindowsHookEx +UnhookWindowsHookEx.argtypes = [HHOOK] +UnhookWindowsHookEx.restype = BOOL + +GetMessage = user32.GetMessageW +GetMessage.argtypes = [LPMSG, HWND, c_uint, c_uint] +GetMessage.restype = BOOL + +TranslateMessage = user32.TranslateMessage +TranslateMessage.argtypes = [LPMSG] +TranslateMessage.restype = BOOL + +DispatchMessage = user32.DispatchMessageA +DispatchMessage.argtypes = [LPMSG] + + +keyboard_state_type = c_uint8 * 256 + +GetKeyboardState = user32.GetKeyboardState +GetKeyboardState.argtypes = [keyboard_state_type] +GetKeyboardState.restype = BOOL + +GetKeyNameText = user32.GetKeyNameTextW +GetKeyNameText.argtypes = [c_long, LPWSTR, c_int] +GetKeyNameText.restype = c_int + +MapVirtualKey = user32.MapVirtualKeyW +MapVirtualKey.argtypes = [c_uint, c_uint] +MapVirtualKey.restype = c_uint + +ToUnicode = user32.ToUnicode +ToUnicode.argtypes = [c_uint, c_uint, keyboard_state_type, LPWSTR, c_int, c_uint] +ToUnicode.restype = c_int + +SendInput = user32.SendInput +SendInput.argtypes = [c_uint, POINTER(INPUT), c_int] +SendInput.restype = c_uint + +# https://msdn.microsoft.com/en-us/library/windows/desktop/ms646307(v=vs.85).aspx +MAPVK_VK_TO_CHAR = 2 +MAPVK_VK_TO_VSC = 0 +MAPVK_VSC_TO_VK = 1 +MAPVK_VK_TO_VSC_EX = 4 +MAPVK_VSC_TO_VK_EX = 3 + +VkKeyScan = user32.VkKeyScanW +VkKeyScan.argtypes = [WCHAR] +VkKeyScan.restype = c_short + +LLKHF_INJECTED = 0x00000010 + +WM_KEYDOWN = 0x0100 +WM_KEYUP = 0x0101 +WM_SYSKEYDOWN = 0x104 # Used for ALT key +WM_SYSKEYUP = 0x105 + + +# This marks the end of Win32 API declarations. The rest is ours. + +keyboard_event_types = { + WM_KEYDOWN: KEY_DOWN, + WM_KEYUP: KEY_UP, + WM_SYSKEYDOWN: KEY_DOWN, + WM_SYSKEYUP: KEY_UP, +} + +# List taken from the official documentation, but stripped of the OEM-specific keys. +# Keys are virtual key codes, values are pairs (name, is_keypad). +official_virtual_keys = { + 0x03: ('control-break processing', False), + 0x08: ('backspace', False), + 0x09: ('tab', False), + 0x0c: ('clear', False), + 0x0d: ('enter', False), + 0x10: ('shift', False), + 0x11: ('ctrl', False), + 0x12: ('alt', False), + 0x13: ('pause', False), + 0x14: ('caps lock', False), + 0x15: ('ime kana mode', False), + 0x15: ('ime hanguel mode', False), + 0x15: ('ime hangul mode', False), + 0x17: ('ime junja mode', False), + 0x18: ('ime final mode', False), + 0x19: ('ime hanja mode', False), + 0x19: ('ime kanji mode', False), + 0x1b: ('esc', False), + 0x1c: ('ime convert', False), + 0x1d: ('ime nonconvert', False), + 0x1e: ('ime accept', False), + 0x1f: ('ime mode change request', False), + 0x20: ('spacebar', False), + 0x21: ('page up', False), + 0x22: ('page down', False), + 0x23: ('end', False), + 0x24: ('home', False), + 0x25: ('left', False), + 0x26: ('up', False), + 0x27: ('right', False), + 0x28: ('down', False), + 0x29: ('select', False), + 0x2a: ('print', False), + 0x2b: ('execute', False), + 0x2c: ('print screen', False), + 0x2d: ('insert', False), + 0x2e: ('delete', False), + 0x2f: ('help', False), + 0x30: ('0', False), + 0x31: ('1', False), + 0x32: ('2', False), + 0x33: ('3', False), + 0x34: ('4', False), + 0x35: ('5', False), + 0x36: ('6', False), + 0x37: ('7', False), + 0x38: ('8', False), + 0x39: ('9', False), + 0x41: ('a', False), + 0x42: ('b', False), + 0x43: ('c', False), + 0x44: ('d', False), + 0x45: ('e', False), + 0x46: ('f', False), + 0x47: ('g', False), + 0x48: ('h', False), + 0x49: ('i', False), + 0x4a: ('j', False), + 0x4b: ('k', False), + 0x4c: ('l', False), + 0x4d: ('m', False), + 0x4e: ('n', False), + 0x4f: ('o', False), + 0x50: ('p', False), + 0x51: ('q', False), + 0x52: ('r', False), + 0x53: ('s', False), + 0x54: ('t', False), + 0x55: ('u', False), + 0x56: ('v', False), + 0x57: ('w', False), + 0x58: ('x', False), + 0x59: ('y', False), + 0x5a: ('z', False), + 0x5b: ('left windows', False), + 0x5c: ('right windows', False), + 0x5d: ('applications', False), + 0x5f: ('sleep', False), + 0x60: ('0', True), + 0x61: ('1', True), + 0x62: ('2', True), + 0x63: ('3', True), + 0x64: ('4', True), + 0x65: ('5', True), + 0x66: ('6', True), + 0x67: ('7', True), + 0x68: ('8', True), + 0x69: ('9', True), + 0x6a: ('*', True), + 0x6b: ('+', True), + 0x6c: ('separator', True), + 0x6d: ('-', True), + 0x6e: ('decimal', True), + 0x6f: ('/', True), + 0x70: ('f1', False), + 0x71: ('f2', False), + 0x72: ('f3', False), + 0x73: ('f4', False), + 0x74: ('f5', False), + 0x75: ('f6', False), + 0x76: ('f7', False), + 0x77: ('f8', False), + 0x78: ('f9', False), + 0x79: ('f10', False), + 0x7a: ('f11', False), + 0x7b: ('f12', False), + 0x7c: ('f13', False), + 0x7d: ('f14', False), + 0x7e: ('f15', False), + 0x7f: ('f16', False), + 0x80: ('f17', False), + 0x81: ('f18', False), + 0x82: ('f19', False), + 0x83: ('f20', False), + 0x84: ('f21', False), + 0x85: ('f22', False), + 0x86: ('f23', False), + 0x87: ('f24', False), + 0x90: ('num lock', False), + 0x91: ('scroll lock', False), + 0xa0: ('left shift', False), + 0xa1: ('right shift', False), + 0xa2: ('left ctrl', False), + 0xa3: ('right ctrl', False), + 0xa4: ('left menu', False), + 0xa5: ('right menu', False), + 0xa6: ('browser back', False), + 0xa7: ('browser forward', False), + 0xa8: ('browser refresh', False), + 0xa9: ('browser stop', False), + 0xaa: ('browser search key', False), + 0xab: ('browser favorites', False), + 0xac: ('browser start and home', False), + 0xad: ('volume mute', False), + 0xae: ('volume down', False), + 0xaf: ('volume up', False), + 0xb0: ('next track', False), + 0xb1: ('previous track', False), + 0xb2: ('stop media', False), + 0xb3: ('play/pause media', False), + 0xb4: ('start mail', False), + 0xb5: ('select media', False), + 0xb6: ('start application 1', False), + 0xb7: ('start application 2', False), + 0xbb: ('+', False), + 0xbc: (',', False), + 0xbd: ('-', False), + 0xbe: ('.', False), + #0xbe:('/', False), # Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the '/?. + 0xe5: ('ime process', False), + 0xf6: ('attn', False), + 0xf7: ('crsel', False), + 0xf8: ('exsel', False), + 0xf9: ('erase eof', False), + 0xfa: ('play', False), + 0xfb: ('zoom', False), + 0xfc: ('reserved ', False), + 0xfd: ('pa1', False), + 0xfe: ('clear', False), +} + +tables_lock = Lock() +to_name = defaultdict(list) +from_name = defaultdict(list) +scan_code_to_vk = {} + +distinct_modifiers = [ + (), + ('shift',), + ('alt gr',), + ('num lock',), + ('shift', 'num lock'), + ('caps lock',), + ('shift', 'caps lock'), + ('alt gr', 'num lock'), +] + +name_buffer = ctypes.create_unicode_buffer(32) +unicode_buffer = ctypes.create_unicode_buffer(32) +keyboard_state = keyboard_state_type() +def get_event_names(scan_code, vk, is_extended, modifiers): + is_keypad = (scan_code, vk, is_extended) in keypad_keys + is_official = vk in official_virtual_keys + if is_keypad and is_official: + yield official_virtual_keys[vk][0] + + keyboard_state[0x10] = 0x80 * ('shift' in modifiers) + keyboard_state[0x11] = 0x80 * ('alt gr' in modifiers) + keyboard_state[0x12] = 0x80 * ('alt gr' in modifiers) + keyboard_state[0x14] = 0x01 * ('caps lock' in modifiers) + keyboard_state[0x90] = 0x01 * ('num lock' in modifiers) + keyboard_state[0x91] = 0x01 * ('scroll lock' in modifiers) + unicode_ret = ToUnicode(vk, scan_code, keyboard_state, unicode_buffer, len(unicode_buffer), 0) + if unicode_ret and unicode_buffer.value: + yield unicode_buffer.value + # unicode_ret == -1 -> is dead key + # ToUnicode has the side effect of setting global flags for dead keys. + # Therefore we need to call it twice to clear those flags. + # If your 6 and 7 keys are named "^6" and "^7", this is the reason. + ToUnicode(vk, scan_code, keyboard_state, unicode_buffer, len(unicode_buffer), 0) + + name_ret = GetKeyNameText(scan_code << 16 | is_extended << 24, name_buffer, 1024) + if name_ret and name_buffer.value: + yield name_buffer.value + + char = user32.MapVirtualKeyW(vk, MAPVK_VK_TO_CHAR) & 0xFF + if char != 0: + yield chr(char) + + if not is_keypad and is_official: + yield official_virtual_keys[vk][0] + +def _setup_name_tables(): + """ + Ensures the scan code/virtual key code/name translation tables are + filled. + """ + with tables_lock: + if to_name: return + + # Go through every possible scan code, and map them to virtual key codes. + # Then vice-versa. + all_scan_codes = [(sc, user32.MapVirtualKeyExW(sc, MAPVK_VSC_TO_VK_EX, 0)) for sc in range(0x100)] + all_vks = [(user32.MapVirtualKeyExW(vk, MAPVK_VK_TO_VSC_EX, 0), vk) for vk in range(0x100)] + for scan_code, vk in all_scan_codes + all_vks: + # `to_name` and `from_name` entries will be a tuple (scan_code, vk, extended, shift_state). + if (scan_code, vk, 0, 0, 0) in to_name: + continue + + if scan_code not in scan_code_to_vk: + scan_code_to_vk[scan_code] = vk + + # Brute force all combinations to find all possible names. + for extended in [0, 1]: + for modifiers in distinct_modifiers: + entry = (scan_code, vk, extended, modifiers) + # Get key names from ToUnicode, GetKeyNameText, MapVirtualKeyW and official virtual keys. + names = list(get_event_names(*entry)) + if names: + # Also map lowercased key names, but only after the properly cased ones. + lowercase_names = [name.lower() for name in names] + to_name[entry] = names + lowercase_names + # Remember the "id" of the name, as the first techniques + # have better results and therefore priority. + for i, name in enumerate(map(normalize_name, names + lowercase_names)): + from_name[name].append((i, entry)) + + # TODO: single quotes on US INTL is returning the dead key (?), and therefore + # not typing properly. + + # Alt gr is way outside the usual range of keys (0..127) and on my + # computer is named as 'ctrl'. Therefore we add it manually and hope + # Windows is consistent in its inconsistency. + for extended in [0, 1]: + for modifiers in distinct_modifiers: + to_name[(541, 162, extended, modifiers)] = ['alt gr'] + from_name['alt gr'].append((1, (541, 162, extended, modifiers))) + + modifiers_preference = defaultdict(lambda: 10) + modifiers_preference.update({(): 0, ('shift',): 1, ('alt gr',): 2, ('ctrl',): 3, ('alt',): 4}) + def order_key(line): + i, entry = line + scan_code, vk, extended, modifiers = entry + return modifiers_preference[modifiers], i, extended, vk, scan_code + for name, entries in list(from_name.items()): + from_name[name] = sorted(set(entries), key=order_key) + +# Called by keyboard/__init__.py +init = _setup_name_tables + +# List created manually. +keypad_keys = [ + # (scan_code, virtual_key_code, is_extended) + (126, 194, 0), + (126, 194, 0), + (28, 13, 1), + (28, 13, 1), + (53, 111, 1), + (53, 111, 1), + (55, 106, 0), + (55, 106, 0), + (69, 144, 1), + (69, 144, 1), + (71, 103, 0), + (71, 36, 0), + (72, 104, 0), + (72, 38, 0), + (73, 105, 0), + (73, 33, 0), + (74, 109, 0), + (74, 109, 0), + (75, 100, 0), + (75, 37, 0), + (76, 101, 0), + (76, 12, 0), + (77, 102, 0), + (77, 39, 0), + (78, 107, 0), + (78, 107, 0), + (79, 35, 0), + (79, 97, 0), + (80, 40, 0), + (80, 98, 0), + (81, 34, 0), + (81, 99, 0), + (82, 45, 0), + (82, 96, 0), + (83, 110, 0), + (83, 46, 0), +] + +shift_is_pressed = False +altgr_is_pressed = False +ignore_next_right_alt = False +shift_vks = set([0x10, 0xa0, 0xa1]) +def prepare_intercept(callback): + """ + Registers a Windows low level keyboard hook. The provided callback will + be invoked for each high-level keyboard event, and is expected to return + True if the key event should be passed to the next program, or False if + the event is to be blocked. + + No event is processed until the Windows messages are pumped (see + start_intercept). + """ + _setup_name_tables() + + def process_key(event_type, vk, scan_code, is_extended): + global shift_is_pressed, altgr_is_pressed, ignore_next_right_alt + #print(event_type, vk, scan_code, is_extended) + + # Pressing alt-gr also generates an extra "right alt" event + if vk == 0xA5 and ignore_next_right_alt: + ignore_next_right_alt = False + return True + + modifiers = ( + ('shift',) * shift_is_pressed + + ('alt gr',) * altgr_is_pressed + + ('num lock',) * (user32.GetKeyState(0x90) & 1) + + ('caps lock',) * (user32.GetKeyState(0x14) & 1) + + ('scroll lock',) * (user32.GetKeyState(0x91) & 1) + ) + entry = (scan_code, vk, is_extended, modifiers) + if entry not in to_name: + to_name[entry] = list(get_event_names(*entry)) + + names = to_name[entry] + name = names[0] if names else None + + # TODO: inaccurate when holding multiple different shifts. + if vk in shift_vks: + shift_is_pressed = event_type == KEY_DOWN + if scan_code == 541 and vk == 162: + ignore_next_right_alt = True + altgr_is_pressed = event_type == KEY_DOWN + + is_keypad = (scan_code, vk, is_extended) in keypad_keys + return callback(KeyboardEvent(event_type=event_type, scan_code=scan_code or -vk, name=name, is_keypad=is_keypad)) + + def low_level_keyboard_handler(nCode, wParam, lParam): + try: + vk = lParam.contents.vk_code + # Ignore the second `alt` DOWN observed in some cases. + fake_alt = (LLKHF_INJECTED | 0x20) + # Ignore events generated by SendInput with Unicode. + if vk != VK_PACKET and lParam.contents.flags & fake_alt != fake_alt: + event_type = keyboard_event_types[wParam] + is_extended = lParam.contents.flags & 1 + scan_code = lParam.contents.scan_code + should_continue = process_key(event_type, vk, scan_code, is_extended) + if not should_continue: + return -1 + except Exception as e: + print('Error in keyboard hook:') + traceback.print_exc() + + return CallNextHookEx(None, nCode, wParam, lParam) + + WH_KEYBOARD_LL = c_int(13) + keyboard_callback = LowLevelKeyboardProc(low_level_keyboard_handler) + handle = GetModuleHandleW(None) + thread_id = DWORD(0) + keyboard_hook = SetWindowsHookEx(WH_KEYBOARD_LL, keyboard_callback, handle, thread_id) + + # Register to remove the hook when the interpreter exits. Unfortunately a + # try/finally block doesn't seem to work here. + atexit.register(UnhookWindowsHookEx, keyboard_callback) + +def listen(callback): + prepare_intercept(callback) + msg = LPMSG() + while not GetMessage(msg, 0, 0, 0): + TranslateMessage(msg) + DispatchMessage(msg) + +def map_name(name): + _setup_name_tables() + + entries = from_name.get(name) + if not entries: + raise ValueError('Key name {} is not mapped to any known key.'.format(repr(name))) + for i, entry in entries: + scan_code, vk, is_extended, modifiers = entry + yield scan_code or -vk, modifiers + +def _send_event(code, event_type): + if code == 541: + # Alt-gr is made of ctrl+alt. Just sending even 541 doesn't do anything. + user32.keybd_event(0x11, code, event_type, 0) + user32.keybd_event(0x12, code, event_type, 0) + elif code > 0: + vk = scan_code_to_vk.get(code, 0) + user32.keybd_event(vk, code, event_type, 0) + else: + # Negative scan code is a way to indicate we don't have a scan code, + # and the value actually contains the Virtual key code. + user32.keybd_event(-code, 0, event_type, 0) + +def press(code): + _send_event(code, 0) + +def release(code): + _send_event(code, 2) + +def type_unicode(character): + # This code and related structures are based on + # http://stackoverflow.com/a/11910555/252218 + surrogates = bytearray(character.encode('utf-16le')) + presses = [] + releases = [] + for i in range(0, len(surrogates), 2): + higher, lower = surrogates[i:i+2] + structure = KEYBDINPUT(0, (lower << 8) + higher, KEYEVENTF_UNICODE, 0, None) + presses.append(INPUT(INPUT_KEYBOARD, _INPUTunion(ki=structure))) + structure = KEYBDINPUT(0, (lower << 8) + higher, KEYEVENTF_UNICODE | KEYEVENTF_KEYUP, 0, None) + releases.append(INPUT(INPUT_KEYBOARD, _INPUTunion(ki=structure))) + inputs = presses + releases + nInputs = len(inputs) + LPINPUT = INPUT * nInputs + pInputs = LPINPUT(*inputs) + cbSize = c_int(ctypes.sizeof(INPUT)) + SendInput(nInputs, pInputs, cbSize) + +if __name__ == '__main__': + _setup_name_tables() + import pprint + pprint.pprint(to_name) + pprint.pprint(from_name) + #listen(lambda e: print(e.to_json()) or True) diff --git a/keyboard/_winmouse.py b/keyboard/_winmouse.py new file mode 100644 index 0000000..ef16dd4 --- /dev/null +++ b/keyboard/_winmouse.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- +import ctypes +import time +from ctypes import c_short, c_char, c_uint8, c_int32, c_int, c_uint, c_uint32, c_long, byref, Structure, CFUNCTYPE, POINTER +from ctypes.wintypes import DWORD, BOOL, HHOOK, MSG, LPWSTR, WCHAR, WPARAM, LPARAM +LPMSG = POINTER(MSG) + +import atexit + +from ._mouse_event import ButtonEvent, WheelEvent, MoveEvent, LEFT, RIGHT, MIDDLE, X, X2, UP, DOWN, DOUBLE, WHEEL, HORIZONTAL, VERTICAL + +#https://github.com/boppreh/mouse/issues/1 +#user32 = ctypes.windll.user32 +user32 = ctypes.WinDLL('user32', use_last_error = True) + +class MSLLHOOKSTRUCT(Structure): + _fields_ = [("x", c_long), + ("y", c_long), + ('data', c_int32), + ('reserved', c_int32), + ("flags", DWORD), + ("time", c_int), + ] + +LowLevelMouseProc = CFUNCTYPE(c_int, WPARAM, LPARAM, POINTER(MSLLHOOKSTRUCT)) + +SetWindowsHookEx = user32.SetWindowsHookExA +#SetWindowsHookEx.argtypes = [c_int, LowLevelMouseProc, c_int, c_int] +SetWindowsHookEx.restype = HHOOK + +CallNextHookEx = user32.CallNextHookEx +#CallNextHookEx.argtypes = [c_int , c_int, c_int, POINTER(MSLLHOOKSTRUCT)] +CallNextHookEx.restype = c_int + +UnhookWindowsHookEx = user32.UnhookWindowsHookEx +UnhookWindowsHookEx.argtypes = [HHOOK] +UnhookWindowsHookEx.restype = BOOL + +GetMessage = user32.GetMessageW +GetMessage.argtypes = [LPMSG, c_int, c_int, c_int] +GetMessage.restype = BOOL + +TranslateMessage = user32.TranslateMessage +TranslateMessage.argtypes = [LPMSG] +TranslateMessage.restype = BOOL + +DispatchMessage = user32.DispatchMessageA +DispatchMessage.argtypes = [LPMSG] + +# Beware, as of 2016-01-30 the official docs have a very incomplete list. +# This one was compiled from experience and may be incomplete. +WM_MOUSEMOVE = 0x200 +WM_LBUTTONDOWN = 0x201 +WM_LBUTTONUP = 0x202 +WM_LBUTTONDBLCLK = 0x203 +WM_RBUTTONDOWN = 0x204 +WM_RBUTTONUP = 0x205 +WM_RBUTTONDBLCLK = 0x206 +WM_MBUTTONDOWN = 0x207 +WM_MBUTTONUP = 0x208 +WM_MBUTTONDBLCLK = 0x209 +WM_MOUSEWHEEL = 0x20A +WM_XBUTTONDOWN = 0x20B +WM_XBUTTONUP = 0x20C +WM_XBUTTONDBLCLK = 0x20D +WM_NCXBUTTONDOWN = 0x00AB +WM_NCXBUTTONUP = 0x00AC +WM_NCXBUTTONDBLCLK = 0x00AD +WM_MOUSEHWHEEL = 0x20E +WM_LBUTTONDOWN = 0x0201 +WM_LBUTTONUP = 0x0202 +WM_MOUSEMOVE = 0x0200 +WM_MOUSEWHEEL = 0x020A +WM_MOUSEHWHEEL = 0x020E +WM_RBUTTONDOWN = 0x0204 +WM_RBUTTONUP = 0x0205 + +buttons_by_wm_code = { + WM_LBUTTONDOWN: (DOWN, LEFT), + WM_LBUTTONUP: (UP, LEFT), + WM_LBUTTONDBLCLK: (DOUBLE, LEFT), + + WM_RBUTTONDOWN: (DOWN, RIGHT), + WM_RBUTTONUP: (UP, RIGHT), + WM_RBUTTONDBLCLK: (DOUBLE, RIGHT), + + WM_MBUTTONDOWN: (DOWN, MIDDLE), + WM_MBUTTONUP: (UP, MIDDLE), + WM_MBUTTONDBLCLK: (DOUBLE, MIDDLE), + + WM_XBUTTONDOWN: (DOWN, X), + WM_XBUTTONUP: (UP, X), + WM_XBUTTONDBLCLK: (DOUBLE, X), +} + +MOUSEEVENTF_ABSOLUTE = 0x8000 +MOUSEEVENTF_MOVE = 0x1 +MOUSEEVENTF_WHEEL = 0x800 +MOUSEEVENTF_HWHEEL = 0x1000 +MOUSEEVENTF_LEFTDOWN = 0x2 +MOUSEEVENTF_LEFTUP = 0x4 +MOUSEEVENTF_RIGHTDOWN = 0x8 +MOUSEEVENTF_RIGHTUP = 0x10 +MOUSEEVENTF_MIDDLEDOWN = 0x20 +MOUSEEVENTF_MIDDLEUP = 0x40 +MOUSEEVENTF_XDOWN = 0x0080 +MOUSEEVENTF_XUP = 0x0100 + +simulated_mouse_codes = { + (WHEEL, HORIZONTAL): MOUSEEVENTF_HWHEEL, + (WHEEL, VERTICAL): MOUSEEVENTF_WHEEL, + + (DOWN, LEFT): MOUSEEVENTF_LEFTDOWN, + (UP, LEFT): MOUSEEVENTF_LEFTUP, + + (DOWN, RIGHT): MOUSEEVENTF_RIGHTDOWN, + (UP, RIGHT): MOUSEEVENTF_RIGHTUP, + + (DOWN, MIDDLE): MOUSEEVENTF_MIDDLEDOWN, + (UP, MIDDLE): MOUSEEVENTF_MIDDLEUP, + + (DOWN, X): MOUSEEVENTF_XDOWN, + (UP, X): MOUSEEVENTF_XUP, +} + +NULL = c_int(0) + +WHEEL_DELTA = 120 + +init = lambda: None + +def listen(queue): + def low_level_mouse_handler(nCode, wParam, lParam): + struct = lParam.contents + # Can't use struct.time because it's usually zero. + t = time.time() + + if wParam == WM_MOUSEMOVE: + event = MoveEvent(struct.x, struct.y, t) + elif wParam == WM_MOUSEWHEEL: + event = WheelEvent(struct.data / (WHEEL_DELTA * (2<<15)), t) + elif wParam in buttons_by_wm_code: + type, button = buttons_by_wm_code.get(wParam, ('?', '?')) + if wParam >= WM_XBUTTONDOWN: + button = {0x10000: X, 0x20000: X2}[struct.data] + event = ButtonEvent(type, button, t) + + queue.put(event) + return CallNextHookEx(NULL, nCode, wParam, lParam) + + WH_MOUSE_LL = c_int(14) + mouse_callback = LowLevelMouseProc(low_level_mouse_handler) + mouse_hook = SetWindowsHookEx(WH_MOUSE_LL, mouse_callback, NULL, NULL) + + # Register to remove the hook when the interpreter exits. Unfortunately a + # try/finally block doesn't seem to work here. + atexit.register(UnhookWindowsHookEx, mouse_hook) + + msg = LPMSG() + while not GetMessage(msg, NULL, NULL, NULL): + TranslateMessage(msg) + DispatchMessage(msg) + +def _translate_button(button): + if button == X or button == X2: + return X, {X: 0x10000, X2: 0x20000}[button] + else: + return button, 0 + +def press(button=LEFT): + button, data = _translate_button(button) + code = simulated_mouse_codes[(DOWN, button)] + user32.mouse_event(code, 0, 0, data, 0) + +def release(button=LEFT): + button, data = _translate_button(button) + code = simulated_mouse_codes[(UP, button)] + user32.mouse_event(code, 0, 0, data, 0) + +def wheel(delta=1): + code = simulated_mouse_codes[(WHEEL, VERTICAL)] + user32.mouse_event(code, 0, 0, int(delta * WHEEL_DELTA), 0) + +def move_to(x, y): + user32.SetCursorPos(int(x), int(y)) + +def move_relative(x, y): + user32.mouse_event(MOUSEEVENTF_MOVE, int(x), int(y), 0, 0) + +class POINT(Structure): + _fields_ = [("x", c_long), ("y", c_long)] + +def get_position(): + point = POINT() + user32.GetCursorPos(byref(point)) + return (point.x, point.y) + +if __name__ == '__main__': + def p(e): + print(e) + listen(p) diff --git a/keyboard/mouse.py b/keyboard/mouse.py new file mode 100644 index 0000000..315199e --- /dev/null +++ b/keyboard/mouse.py @@ -0,0 +1,232 @@ +# -*- coding: utf-8 -*- +import warnings +warnings.simplefilter('always', DeprecationWarning) +warnings.warn('The mouse sub-library is deprecated and will be removed in future versions. Please use the standalone package `mouse`.', DeprecationWarning, stacklevel=2) + +import time as _time + +import platform as _platform +if _platform.system() == 'Windows': + from. import _winmouse as _os_mouse +elif _platform.system() == 'Linux': + from. import _nixmouse as _os_mouse +elif _platform.system() == 'Darwin': + from. import _darwinmouse as _os_mouse +else: + raise OSError("Unsupported platform '{}'".format(_platform.system())) + +from ._mouse_event import ButtonEvent, MoveEvent, WheelEvent, LEFT, RIGHT, MIDDLE, X, X2, UP, DOWN, DOUBLE +from ._generic import GenericListener as _GenericListener + +_pressed_events = set() +class _MouseListener(_GenericListener): + def init(self): + _os_mouse.init() + def pre_process_event(self, event): + if isinstance(event, ButtonEvent): + if event.event_type in (UP, DOUBLE): + _pressed_events.discard(event.button) + else: + _pressed_events.add(event.button) + return True + + def listen(self): + _os_mouse.listen(self.queue) + +_listener = _MouseListener() + +def is_pressed(button=LEFT): + """ Returns True if the given button is currently pressed. """ + _listener.start_if_necessary() + return button in _pressed_events + +def press(button=LEFT): + """ Presses the given button (but doesn't release). """ + _os_mouse.press(button) + +def release(button=LEFT): + """ Releases the given button. """ + _os_mouse.release(button) + +def click(button=LEFT): + """ Sends a click with the given button. """ + _os_mouse.press(button) + _os_mouse.release(button) + +def double_click(button=LEFT): + """ Sends a double click with the given button. """ + click(button) + click(button) + +def right_click(): + """ Sends a right click with the given button. """ + click(RIGHT) + +def wheel(delta=1): + """ Scrolls the wheel `delta` clicks. Sign indicates direction. """ + _os_mouse.wheel(delta) + +def move(x, y, absolute=True, duration=0): + """ + Moves the mouse. If `absolute`, to position (x, y), otherwise move relative + to the current position. If `duration` is non-zero, animates the movement. + """ + x = int(x) + y = int(y) + + # Requires an extra system call on Linux, but `move_relative` is measured + # in millimiters so we would lose precision. + position_x, position_y = get_position() + + if not absolute: + x = position_x + x + y = position_y + y + + if duration: + start_x = position_x + start_y = position_y + dx = x - start_x + dy = y - start_y + + if dx == 0 and dy == 0: + _time.sleep(duration) + else: + # 120 movements per second. + # Round and keep float to ensure float division in Python 2 + steps = max(1.0, float(int(duration * 120.0))) + for i in range(int(steps)+1): + move(start_x + dx*i/steps, start_y + dy*i/steps) + _time.sleep(duration/steps) + else: + _os_mouse.move_to(x, y) + +def drag(start_x, start_y, end_x, end_y, absolute=True, duration=0): + """ + Holds the left mouse button, moving from start to end position, then + releases. `absolute` and `duration` are parameters regarding the mouse + movement. + """ + if is_pressed(): + release() + move(start_x, start_y, absolute, 0) + press() + move(end_x, end_y, absolute, duration) + release() + +def on_button(callback, args=(), buttons=(LEFT, MIDDLE, RIGHT, X, X2), types=(UP, DOWN, DOUBLE)): + """ Invokes `callback` with `args` when the specified event happens. """ + if not isinstance(buttons, (tuple, list)): + buttons = (buttons,) + if not isinstance(types, (tuple, list)): + types = (types,) + + def handler(event): + if isinstance(event, ButtonEvent): + if event.event_type in types and event.button in buttons: + callback(*args) + _listener.add_handler(handler) + return handler + +def on_click(callback, args=()): + """ Invokes `callback` with `args` when the left button is clicked. """ + return on_button(callback, args, [LEFT], [UP]) + +def on_double_click(callback, args=()): + """ + Invokes `callback` with `args` when the left button is double clicked. + """ + return on_button(callback, args, [LEFT], [DOUBLE]) + +def on_right_click(callback, args=()): + """ Invokes `callback` with `args` when the right button is clicked. """ + return on_button(callback, args, [RIGHT], [UP]) + +def on_middle_click(callback, args=()): + """ Invokes `callback` with `args` when the middle button is clicked. """ + return on_button(callback, args, [MIDDLE], [UP]) + +def wait(button=LEFT, target_types=(UP, DOWN, DOUBLE)): + """ + Blocks program execution until the given button performs an event. + """ + from threading import Lock + lock = Lock() + lock.acquire() + handler = on_button(lock.release, (), [button], target_types) + lock.acquire() + _listener.remove_handler(handler) + +def get_position(): + """ Returns the (x, y) mouse position. """ + return _os_mouse.get_position() + +def hook(callback): + """ + Installs a global listener on all available mouses, invoking `callback` + each time it is moved, a key status changes or the wheel is spun. A mouse + event is passed as argument, with type either `mouse.ButtonEvent`, + `mouse.WheelEvent` or `mouse.MoveEvent`. + + Returns the given callback for easier development. + """ + _listener.add_handler(callback) + return callback + +def unhook(callback): + """ + Removes a previously installed hook. + """ + _listener.remove_handler(callback) + +def unhook_all(): + """ + Removes all hooks registered by this application. Note this may include + hooks installed by high level functions, such as `record`. + """ + del _listener.handlers[:] + +def record(button=RIGHT, target_types=(DOWN,)): + """ + Records all mouse events until the user presses the given button. + Then returns the list of events recorded. Pairs well with `play(events)`. + + Note: this is a blocking function. + Note: for more details on the mouse hook and events see `hook`. + """ + recorded = [] + hook(recorded.append) + wait(button=button, target_types=target_types) + unhook(recorded.append) + return recorded + +def play(events, speed_factor=1.0, include_clicks=True, include_moves=True, include_wheel=True): + """ + Plays a sequence of recorded events, maintaining the relative time + intervals. If speed_factor is <= 0 then the actions are replayed as fast + as the OS allows. Pairs well with `record()`. + + The parameters `include_*` define if events of that type should be inluded + in the replay or ignored. + """ + last_time = None + for event in events: + if speed_factor > 0 and last_time is not None: + _time.sleep((event.time - last_time) / speed_factor) + last_time = event.time + + if isinstance(event, ButtonEvent) and include_clicks: + if event.event_type == UP: + _os_mouse.release(event.button) + else: + _os_mouse.press(event.button) + elif isinstance(event, MoveEvent) and include_moves: + _os_mouse.move_to(event.x, event.y) + elif isinstance(event, WheelEvent) and include_wheel: + _os_mouse.wheel(event.delta) + +replay = play +hold = press + +if __name__ == '__main__': + print('Recording... Double click to stop and replay.') + play(record()) diff --git a/main.py b/main.py new file mode 100644 index 0000000..2048a11 --- /dev/null +++ b/main.py @@ -0,0 +1,231 @@ +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +from ui_mainwnd import Ui_MainWidget +from profileeditwnd import ProfileEditWnd +import json +from profileexecutor import ProfileExecutor +import sys +import pickle +import time +import keyboard +import threading + +class MainWnd(QWidget): + def __init__(self, p_parent = None): + super().__init__(p_parent) + self.ui = Ui_MainWidget() + self.ui.setupUi(self) + self.m_profileExecutor = ProfileExecutor() + + self.ui.profileCbx.currentIndexChanged.connect(self.slotProfileChanged) + self.ui.addBut.clicked.connect(self.slotAddNewProfile) + self.ui.editBut.clicked.connect(self.slotEditProfile) + self.ui.removeBut.clicked.connect(self.slotRemoveProfile) + self.ui.listeningChk.stateChanged.connect(self.slotListeningEnabled) + self.ui.ok.clicked.connect(self.slotOK) + self.ui.cancel.clicked.connect(self.slotCancel) + + self.m_profileExecutor.start() + + if self.loadFromDatabase() > 0 : + # if self.loadTestProfiles() > 0: + w_jsonProfile = self.ui.profileCbx.itemData(0) + if w_jsonProfile != None: + self.m_activeProfile = json.loads(w_jsonProfile) + self.m_profileExecutor.setProfile(self.m_activeProfile) + + def saveToDatabase(self): + w_profiles = [] + w_profileCnt = self.ui.profileCbx.count() + for w_idx in range(w_profileCnt): + w_jsonProfile = self.ui.profileCbx.itemData(w_idx) + if w_jsonProfile == None: + continue + + w_profile = json.loads(w_jsonProfile) + w_profiles.append(w_profile) + + with open("profiles.dat", "wb") as f: + pickle.dump(w_profiles, f, pickle.HIGHEST_PROTOCOL) + + def loadFromDatabase(self): + w_profiles = [] + with open("profiles.dat", "rb") as f: + w_profiles = pickle.load(f) + + for w_profile in w_profiles: + self.ui.profileCbx.addItem(w_profile['name']) + w_jsonProfile = json.dumps(w_profile) + self.ui.profileCbx.setItemData(self.ui.profileCbx.count() - 1, w_jsonProfile) + + return len(w_profiles) + + def loadTestProfiles(self): + + w_carProfileDict = { + "name": "car game", + "commands": [ + {'name': 'up', + 'actions': [ + {'name': 'key action', 'key': 'up', 'type': 1}, + {'name': 'pause action', 'time': 0.03} + ], + 'repeat': 1, + 'async': False + }, + {'name': 'left', + 'actions': [{'name': 'key action', 'key': 'right', 'type': 0}, + {'name': 'pause action', 'time': 0.03}, + {'name': 'key action', 'key': 'left', 'type': 1}, + {'name': 'pause action', 'time': 0.03} + ], + 'repeat': 1, + 'async': False + }, + {'name': 'right', + 'actions': [{'name': 'key action', 'key': 'left', 'type': 0}, + {'name': 'pause action', 'time': 0.03}, + {'name': 'key action', 'key': 'right', 'type': 1}, + {'name': 'pause action', 'time': 0.03} + ], + 'repeat': 1, + 'async': False + }, + {'name': 'stop', + 'actions': [ + {'name': 'key action', 'key': 'left', 'type': 0}, + {'name': 'pause action', 'time': 0.03}, + {'name': 'key action', 'key': 'right', 'type': 0}, + {'name': 'pause action', 'time': 0.03}, + {'name': 'key action', 'key': 'up', 'type': 0}, + {'name': 'pause action', 'time': 0.03} + ], + 'repeat': 1, + 'async': False + } + ] + } + + w_airplaneProfileDict = { + "name": "airplane game", + "commands": [ + {'name': 'up', + 'actions': [ + {'name': 'command stop action', 'command name': 'down'}, + {'name': 'mouse move action', 'x': 0, 'y': -5, 'absolute': False}, + {'name': 'pause action', 'time': 0.01} + ], + 'repeat': -1, + 'async': True + }, + {'name': 'left', + 'actions': [ + {'name': 'command stop action', 'command name': 'right'}, + {'name': 'mouse move action', 'x': -5, 'y': 0, 'absolute': False}, + {'name': 'pause action', 'time': 0.005} + ], + 'repeat': -1, + 'async': True + }, + {'name': 'right', + 'actions': [ + {'name': 'command stop action', 'command name': 'left'}, + {'name': 'mouse move action', 'x': 5, 'y': 0, 'absolute': False}, + {'name': 'pause action', 'time': 0.005} + ], + 'repeat': -1, + 'async': True + }, + {'name': 'down', + 'actions': [ + {'name': 'command stop action', 'command name': 'up'}, + {'name': 'mouse move action', 'x': 0, 'y': 5, 'absolute': False}, + {'name': 'pause action', 'time': 0.005} + ], + 'repeat': -1, + 'async': True + }, + {'name': 'shoot', + 'actions': [ + {'name': 'mouse click action', 'button': 'left', 'type': 1}, + {'name': 'pause action', 'time': 0.03} + ], + 'repeat': 1, + 'async': False + }, + {'name': 'stop', + 'actions': [ + {'name': 'command stop action', 'command name': 'up'}, + {'name': 'command stop action', 'command name': 'left'}, + {'name': 'command stop action', 'command name': 'right'}, + {'name': 'command stop action', 'command name': 'down'}, + {'name': 'mouse click action', 'button': 'left', 'type': 0} + ], + 'repeat': 1, + 'async': False + } + ] + } + w_profiles = [] + w_profiles.append(w_carProfileDict) + w_profiles.append(w_airplaneProfileDict) + + i = 0 + for w_profile in w_profiles: + self.ui.profileCbx.addItem(w_profile['name']) + w_jsonProfile = json.dumps(w_profile) + self.ui.profileCbx.setItemData(i, w_jsonProfile) + i = i + 1 + + return i + + def slotProfileChanged(self, p_idx): + w_jsonProfile = self.ui.profileCbx.itemData(p_idx) + if w_jsonProfile != None: + self.m_activeProfile = json.loads(w_jsonProfile) + self.m_profileExecutor.setProfile(self.m_activeProfile) + + def slotAddNewProfile(self): + w_profileEditWnd = ProfileEditWnd(None, self) + if w_profileEditWnd.exec() == QDialog.Accepted: + w_profile = w_profileEditWnd.m_profile + self.ui.profileCbx.addItem(w_profile['name']) + w_jsonProfile = json.dumps(w_profile) + self.ui.profileCbx.setItemData(self.ui.profileCbx.count()-1, w_jsonProfile) + + def slotEditProfile(self): + w_idx = self.ui.profileCbx.currentIndex() + w_jsonProfile = self.ui.profileCbx.itemData(w_idx) + w_profile = json.loads(w_jsonProfile) + w_profileEditWnd = ProfileEditWnd(w_profile, self) + if w_profileEditWnd.exec() == QDialog.Accepted: + w_profile = w_profileEditWnd.m_profile + self.ui.profileCbx.setItemText(w_idx, w_profile['name']) + w_jsonProfile = json.dumps(w_profile) + self.ui.profileCbx.setItemData(w_idx, w_jsonProfile) + + def slotRemoveProfile(self): + w_curIdx = self.ui.profileCbx.currentIndex() + if w_curIdx >= 0: + self.ui.profileCbx.removeItem(w_curIdx) + + def slotListeningEnabled(self, p_enabled): + if p_enabled: + self.m_profileExecutor.setEnableListening(True) + else: + self.m_profileExecutor.setEnableListening(False) + + def slotOK(self): + self.saveToDatabase() + self.close() + + def slotCancel(self): + self.close() + exit() + +if __name__ == "__main__": + app = QApplication(sys.argv) + mainWnd = MainWnd() + mainWnd.show() + sys.exit(app.exec_()) diff --git a/mainwnd.ui b/mainwnd.ui new file mode 100644 index 0000000..790a116 --- /dev/null +++ b/mainwnd.ui @@ -0,0 +1,167 @@ + + + MainWidget + + + + 0 + 0 + 820 + 166 + + + + + 10 + + + + LinVAM + + + + 20 + + + 20 + + + + + 20 + + + + + Enable Listening + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 130 + 0 + + + + OK + + + + + + + + 130 + 0 + + + + Cancel + + + + + + + + + 20 + + + + + + 60 + 0 + + + + Profile: + + + + + + + + 250 + 0 + + + + + + + + + 130 + 0 + + + + Add + + + + + + + + 130 + 0 + + + + Edit + + + + + + + + 130 + 0 + + + + Remove + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + diff --git a/mouseactioneditwnd.py b/mouseactioneditwnd.py new file mode 100644 index 0000000..b741b2f --- /dev/null +++ b/mouseactioneditwnd.py @@ -0,0 +1,117 @@ +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +from ui_mouseactioneditwnd import Ui_MouseActionEditDialog +import json + + +class MouseActionEditWnd(QDialog): + def __init__(self, p_mouseAction, p_parent = None): + super().__init__(p_parent) + self.ui = Ui_MouseActionEditDialog() + self.ui.setupUi(self) + + self.ui.ok.clicked.connect(self.slotOK) + self.ui.cancel.clicked.connect(super().reject) + + self.m_mouseAction = {} + + if p_mouseAction == None: + return + + if p_mouseAction['name'] == 'mouse click action': + self.ui.mouseActionTabWidget.setCurrentIndex(0) + if p_mouseAction['button'] == 'left': + if p_mouseAction['type'] == 10: # click + self.ui.leftClick.setChecked(True) + elif p_mouseAction['type'] == 1: # down + self.ui.leftDown.setChecked(True) + elif p_mouseAction['type'] == 0: # up + self.ui.leftUp.setChecked(True) + elif p_mouseAction['type'] == 11: # double-click + self.ui.leftDclick.setChecked(True) + elif p_mouseAction['button'] == 'right': + if p_mouseAction['type'] == 10: + self.ui.rightClick.setChecked(True) + elif p_mouseAction['type'] == 1: + self.ui.rightDown.setChecked(True) + elif p_mouseAction['type'] == 0: + self.ui.rightUp.setChecked(True) + elif p_mouseAction['type'] == 11: # double-click + self.ui.rightDclick.setChecked(True) + elif p_mouseAction['button'] == 'middle': + if p_mouseAction['type'] == 10: + self.ui.middleClick.setChecked(True) + elif p_mouseAction['type'] == 1: + self.ui.middleDown.setChecked(True) + elif p_mouseAction['type'] == 0: + self.ui.middleUp.setChecked(True) + elif p_mouseAction['type'] == 11: # double-click + self.ui.middleDclick.setChecked(True) + elif p_mouseAction['name'] == 'mouse move action': + self.ui.mouseActionTabWidget.setCurrentIndex(1) + if p_mouseAction['absolute'] == True: + self.ui.moveTo.setChecked(True) + self.ui.xEdit.setText(str(p_mouseAction['x'])) + self.ui.yEdit.setText(str(p_mouseAction['y'])) + else: + self.ui.moveOffset.setChecked(True) + self.ui.xOffsetEdit.setText(str(p_mouseAction['x'])) + self.ui.yOffsetEdit.setText(str(p_mouseAction['y'])) + elif p_mouseAction['name'] == 'mouse scroll action': + self.ui.mouseActionTabWidget.setCurrentIndex(1) + if p_mouseAction['delta'] < 0: + self.ui.scrollUp.setChecked(True) + self.ui.scrollUpEdit.setText(str(-p_mouseAction['delta'])) + else: + self.ui.scrollDown.setChecked(True) + self.ui.scrollDownEdit.setText(str(p_mouseAction['delta'])) + + def slotOK(self): + if self.ui.mouseActionTabWidget.currentIndex() == 0: + if self.ui.leftClick.isChecked(): + self.m_mouseAction = {'name':'mouse click action', 'button':'left', 'type':10} + elif self.ui.rightClick.isChecked(): + self.m_mouseAction = {'name': 'mouse click action', 'button': 'right', 'type': 10} + elif self.ui.middleClick.isChecked(): + self.m_mouseAction = {'name': 'mouse click action', 'button': 'middle', 'type': 10} + + elif self.ui.leftDclick.isChecked(): + self.m_mouseAction = {'name':'mouse click action', 'button':'left', 'type':11} + elif self.ui.rightDclick.isChecked(): + self.m_mouseAction = {'name': 'mouse click action', 'button': 'right', 'type': 11} + elif self.ui.middleDclick.isChecked(): + self.m_mouseAction = {'name': 'mouse click action', 'button': 'middle', 'type': 11} + + elif self.ui.leftDown.isChecked(): + self.m_mouseAction = {'name': 'mouse click action', 'button': 'left', 'type': 1} + elif self.ui.rightDown.isChecked(): + self.m_mouseAction = {'name': 'mouse click action', 'button': 'right', 'type': 1} + elif self.ui.middleDown.isChecked(): + self.m_mouseAction = {'name': 'mouse click action', 'button': 'middle', 'type': 1} + + elif self.ui.leftUp.isChecked(): + self.m_mouseAction = {'name': 'mouse click action', 'button': 'left', 'type': 0} + elif self.ui.rightUp.isChecked(): + self.m_mouseAction = {'name': 'mouse click action', 'button': 'right', 'type': 0} + elif self.ui.middleUp.isChecked(): + self.m_mouseAction = {'name': 'mouse click action', 'button': 'middle', 'type': 0} + else: + if self.ui.moveTo.isChecked(): + self.m_mouseAction['name'] = 'mouse move action' + self.m_mouseAction['x'] = int(self.ui.xEdit.text()) + self.m_mouseAction['y'] = int(self.ui.yEdit.text()) + self.m_mouseAction['absolute'] = True + elif self.ui.moveOffset.isChecked(): + self.m_mouseAction['name'] = 'mouse move action' + self.m_mouseAction['x'] = int(self.ui.xOffsetEdit.text()) + self.m_mouseAction['y'] = int(self.ui.yOffsetEdit.text()) + self.m_mouseAction['absolute'] = False + elif self.ui.scrollUp.isChecked(): + self.m_mouseAction['name'] = 'mouse scroll action' + self.m_mouseAction['delta'] = -(abs(int(self.ui.scrollUpEdit.text()))) + elif self.ui.scrollDown.isChecked(): + self.m_mouseAction['name'] = 'mouse scroll action' + self.m_mouseAction['delta'] = abs(int(self.ui.scrollDownEdit.text())) + + super().accept() \ No newline at end of file diff --git a/mouseactioneditwnd.ui b/mouseactioneditwnd.ui new file mode 100644 index 0000000..8f6cce0 --- /dev/null +++ b/mouseactioneditwnd.ui @@ -0,0 +1,402 @@ + + + MouseActionEditDialog + + + + 0 + 0 + 651 + 268 + + + + + 10 + + + + Dialog + + + + 20 + + + + + 0 + + + + Click + + + + 20 + + + 9 + + + + + Click left button + + + + + + + Click right button + + + + + + + Click middle button + + + + + + + Double-click left button + + + + + + + Double-click right button + + + + + + + Double-click middle button + + + + + + + left button down + + + + + + + right button down + + + + + + + middle button down + + + + + + + left button up + + + + + + + right button up + + + + + + + middle button up + + + + + + + + Move + + + + 20 + + + + + + + + 100 + 0 + + + + Move to: + + + + + + + + 25 + 0 + + + + X + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + 25 + 0 + + + + Y + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + + + 100 + 0 + + + + Move offset + + + true + + + + + + + + 25 + 0 + + + + X + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + 70 + 0 + + + + pixels + + + + + + + + 25 + 0 + + + + Y + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + 70 + 0 + + + + pixels + + + + + + + + + + + + 100 + 0 + + + + Scroll up + + + true + + + + + + + + + + + 80 + 0 + + + + pixels + + + + + + + + 110 + 0 + + + + Scroll down + + + true + + + + + + + + + + + 80 + 0 + + + + pixels + + + + + + + + + + + + + 20 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 130 + 0 + + + + OK + + + false + + + + + + + + 130 + 0 + + + + Cancel + + + false + + + + + + + + + + diff --git a/pauseactioneditwnd.py b/pauseactioneditwnd.py new file mode 100644 index 0000000..7b5b169 --- /dev/null +++ b/pauseactioneditwnd.py @@ -0,0 +1,27 @@ +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +from ui_pauseactioneditwnd import Ui_PauseActionEditDialog +import json + +class PauseActionEditWnd(QDialog): + def __init__(self, p_pauseAction, p_parent = None): + super().__init__(p_parent) + self.ui = Ui_PauseActionEditDialog() + self.ui.setupUi(self) + + self.ui.ok.clicked.connect(self.slotOK) + self.ui.cancel.clicked.connect(super().reject) + + self.m_pauseAction = {} + + if p_pauseAction == None: + return + + self.ui.secondEdit.setText(str(p_pauseAction['time'])) + + def slotOK(self): + self.m_pauseAction['name'] = 'pause action' + self.m_pauseAction['time'] = float(self.ui.secondEdit.text()) + super().accept() + diff --git a/pauseactioneditwnd.ui b/pauseactioneditwnd.ui new file mode 100644 index 0000000..3ac4aee --- /dev/null +++ b/pauseactioneditwnd.ui @@ -0,0 +1,93 @@ + + + PauseActionEditDialog + + + + 0 + 0 + 271 + 133 + + + + + 10 + + + + Pause Action Edit Dialog + + + + + + + + Pause for + + + + + + + + + + seconds + + + + + + + + + 20 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 100 + 0 + + + + OK + + + + + + + + 100 + 0 + + + + Cancel + + + + + + + + + + diff --git a/profileeditwnd.py b/profileeditwnd.py new file mode 100644 index 0000000..d1fa991 --- /dev/null +++ b/profileeditwnd.py @@ -0,0 +1,221 @@ +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +from ui_profileeditwnd import Ui_ProfileEditDialog +from commandeditwnd import CommandEditWnd +import json +from profileexecutor import * +import sys + +class ProfileEditWnd(QDialog): + def __init__(self, p_profile, p_parent = None): + super().__init__(p_parent) + self.m_profiles = [] + self.ui = Ui_ProfileEditDialog() + self.ui.setupUi(self) + self.m_profile = {} + + self.ui.cmdTable.setHorizontalHeaderLabels(('Spoken command', 'Actions')) + self.ui.cmdTable.setSelectionBehavior(QAbstractItemView.SelectRows) + self.ui.cmdTable.setSelectionMode(QAbstractItemView.SingleSelection) + + self.ui.cmdTable.horizontalHeader().setSectionResizeMode(0, QHeaderView.Interactive) + self.ui.cmdTable.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch) + + # define actions here + self.ui.newCmd.clicked.connect(self.slotNewCmd) + self.ui.editCmd.clicked.connect(self.slotEditCmd) + self.ui.cmdTable.doubleClicked.connect(self.slotEditCmd) + self.ui.deleteCmd.clicked.connect(self.slotDeleteCmd) + self.ui.ok.clicked.connect(self.slotOK) + self.ui.cancel.clicked.connect(self.slotCancel) + + if p_profile == None or p_profile == {}: + self.ui.cmdTable.setRowCount(0) + return + + self.ui.profileNameEdit.setText(p_profile['name']) + w_commands = p_profile['commands'] + self.ui.cmdTable.setRowCount(len(w_commands)) + i = 0 + for w_command in w_commands: + self.ui.cmdTable.setItem(i, 0, QTableWidgetItem(w_command['name'])) + w_text = json.dumps(w_command) + w_item = QTableWidgetItem(w_text) + w_item.setData(Qt.UserRole, json.dumps(w_command)) + self.ui.cmdTable.setItem(i, 1, w_item) + i = i + 1 + + QTimer.singleShot(100, self.ui.cmdTable.resizeRowsToContents) + + # def activateProfile(self, p_profile): + # if p_profile == None or p_profile == {}: + # self.ui.cmdTable.setRowCount(0) + # return + # + # w_commands = p_profile['commands'] + # self.ui.cmdTable.setRowCount(len(w_commands)) + # i = 0 + # for w_command in w_commands: + # self.ui.cmdTable.setItem(i, 0, QTableWidgetItem(w_command['name'])) + # w_text = json.dumps(w_command) + # w_item = QTableWidgetItem(w_text) + # w_item.setData(Qt.UserRole, json.dumps(w_command)) + # self.ui.cmdTable.setItem(i, 1, w_item) + # i = i + 1 + # self.ui.cmdTable.resizeRowsToContents() + + # def updateActiveProfile(self): + # w_idx = self.m_curProfileIdx + # if w_idx < 0: + # return + # + # w_profile = {} + # w_profile['name'] = self.ui.profileCbx.itemText(w_idx) + # w_commands = [] + # + # w_commandCnt = self.ui.cmdTable.rowCount() + # for w_i in range(w_commandCnt): + # w_jsonCommand = self.ui.cmdTable.item(w_i, 1).data(Qt.UserRole) + # w_command = json.loads(w_jsonCommand) + # w_commands.append(w_command) + # w_profile['commands'] = w_commands + # w_jsonProfile = json.dumps(w_profile) + # self.ui.profileCbx.setItemData(w_idx, w_jsonProfile) + + # def slotSelChanged(self, p_idx): + # self.updateActiveProfile() + # w_jsonProfile = self.ui.profileCbx.itemData(p_idx) + # + # w_profile = {} + # if w_jsonProfile != None: + # w_profile = json.loads(w_jsonProfile) + # + # self.activateProfile(w_profile) + # + # self.m_curProfileIdx = p_idx + + # def importProfile(self, p_profile, p_update = True): + # if p_profile == None or p_profile == {}: + # return + # + # w_profileCnt = self.ui.profileCbx.count() + # for w_idx in range(w_profileCnt): + # w_jsonProfile = self.ui.profileCbx.itemData(w_idx) + # if w_jsonProfile == None: + # continue + # + # w_profile = json.loads(w_jsonProfile) + # if p_profile['name'] == w_profile['name']: + # if p_update: + # w_jsonProfile = json.dumps(p_profile) + # self.ui.profileCbx.setItemData(w_idx, w_jsonProfile) + # return True + # return False + # + # self.ui.profileCbx.addItem(p_profile['name']) + # w_jsonProfile = json.dumps(p_profile) + # self.ui.profileCbx.setItemData(w_profileCnt, w_jsonProfile) + # return True + + # def slotAddNewProfile(self): + # text, okPressed = QInputDialog.getText(self, "Get Profile Name", "Profile name:", QLineEdit.Normal, "") + # if okPressed and text != '': + # w_profile = {} + # w_profile['name'] = text + # w_profile['commands'] = [] + # if self.importProfile(w_profile, False) == False: + # QMessageBox.critical(None, 'Error', 'Adding a new profile was failed') + # return + # self.ui.profileCbx.setCurrentIndex(self.ui.profileCbx.count()-1) + + # def slotRemoveProfile(self): + # w_curIdx = self.ui.profileCbx.currentIndex() + # if w_curIdx >= 0: + # self.ui.cmdTable.setRowCount(0) + # self.m_curProfileIdx = -1 + # self.ui.profileCbx.removeItem(w_curIdx) + # + # def slotRenameProfile(self): + # w_curIdx = self.ui.profileCbx.currentIndex() + # if w_curIdx >= 0: + # text, okPressed = QInputDialog.getText(self, "Input Dialog", "Profile name:", + # QLineEdit.Normal, self.ui.profileCbx.itemText(w_curIdx)) + # if okPressed and text != '': + # self.ui.profileCbx.setItemText(w_curIdx, text) + # w_jsonProfile = self.ui.profileCbx.itemData(w_curIdx) + # w_profile = json.loads(w_jsonProfile) + # w_profile['name'] = text + # w_jsonProfile = json.dumps(w_profile) + # self.ui.profileCbx.setItemData(w_curIdx, w_jsonProfile) + + def importCommand(self, p_command, p_update): + w_commandCnt = self.ui.cmdTable.rowCount() + for w_i in range(w_commandCnt): + w_jsonCommand = self.ui.cmdTable.item(w_i, 1).data(Qt.UserRole) + w_command = json.loads(w_jsonCommand) + if w_command['name'] == p_command['name']: + if p_update: + w_text = json.dumps(p_command) + w_item = QTableWidgetItem(w_text) + w_item.setData(Qt.UserRole, json.dumps(p_command)) + self.ui.cmdTable.setItem(w_i, 1, w_item) + self.ui.cmdTable.resizeRowsToContents() + return True + return False + + w_rowCnt = self.ui.cmdTable.rowCount() + self.ui.cmdTable.setRowCount(w_rowCnt + 1) + self.ui.cmdTable.setItem(w_rowCnt, 0, QTableWidgetItem(p_command['name'])) + w_text = json.dumps(p_command) + w_item = QTableWidgetItem(w_text) + w_item.setData(Qt.UserRole, json.dumps(p_command)) + self.ui.cmdTable.setItem(w_rowCnt, 1, w_item) + self.ui.cmdTable.resizeRowsToContents() + + def slotNewCmd(self): + w_cmdEditWnd = CommandEditWnd(None, self) + if w_cmdEditWnd.exec_() == QDialog.Accepted: + if self.importCommand(w_cmdEditWnd.m_command, False) == False: + QMessageBox.critical(None, 'Error', 'Adding a new command was failed') + return + self.ui.cmdTable.selectRow(self.ui.cmdTable.rowCount() - 1) + self.ui.cmdTable.setFocus() + + def slotEditCmd(self): + w_modelIdxs = self.ui.cmdTable.selectionModel().selectedRows() + if len(w_modelIdxs) == 0: + return + w_modelIdx = w_modelIdxs[0] + w_jsonCommand = self.ui.cmdTable.item(w_modelIdx.row(), 1).data(Qt.UserRole) + w_command = json.loads(w_jsonCommand) + + w_cmdEditWnd = CommandEditWnd(w_command, self) + if w_cmdEditWnd.exec_() == QDialog.Accepted: + self.importCommand(w_cmdEditWnd.m_command, True) + self.ui.cmdTable.resizeRowsToContents() + + def slotDeleteCmd(self): + w_modelIdxs = self.ui.cmdTable.selectionModel().selectedRows() + i = 0 + for w_modelIdx in sorted(w_modelIdxs): + self.ui.cmdTable.removeRow(w_modelIdx.row() - i) + i = i + 1 + self.ui.cmdTable.setFocus() + + def slotOK(self): + self.m_profile = {} + self.m_profile['name'] = self.ui.profileNameEdit.text() + w_commands = [] + + w_commandCnt = self.ui.cmdTable.rowCount() + for w_i in range(w_commandCnt): + w_jsonCommand = self.ui.cmdTable.item(w_i, 1).data(Qt.UserRole) + w_command = json.loads(w_jsonCommand) + w_commands.append(w_command) + self.m_profile['commands'] = w_commands + + super().accept() + + def slotCancel(self): + super().reject() diff --git a/profileeditwnd.ui b/profileeditwnd.ui new file mode 100644 index 0000000..a61e553 --- /dev/null +++ b/profileeditwnd.ui @@ -0,0 +1,219 @@ + + + ProfileEditDialog + + + + 0 + 0 + 1191 + 591 + + + + Dialog + + + + + + + + + 60 + 0 + + + + Profile: + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Commands + + + + + + + 0 + 200 + + + + + 11 + + + + QAbstractItemView::NoEditTriggers + + + false + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + 2 + + + 160 + + + 160 + + + + + + + + + 20 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 130 + 0 + + + + New + + + + + + + + 130 + 0 + + + + Edit + + + + + + + + 130 + 0 + + + + Delete + + + + + + + + + + + + 20 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 130 + 0 + + + + OK + + + + + + + + 130 + 0 + + + + Cancel + + + + + + + + + + diff --git a/profileexecutor.py b/profileexecutor.py new file mode 100644 index 0000000..e51cc43 --- /dev/null +++ b/profileexecutor.py @@ -0,0 +1,318 @@ +import keyboard +from pynput.mouse import Button, Controller +import time +import threading +import copy +import json + +class ProfileExecutor(threading.Thread): + mouse = Controller() + + def __init__(self, p_profile = None): + threading.Thread.__init__(self) + self.m_profile = p_profile + self.m_stop = False + self.m_listening = True + self.m_cmdThreads = {} + + def setProfile(self, p_profile): + self.m_profile = p_profile + + def setEnableListening(self, p_enable): + self.m_listening = p_enable + + def run(self): + while self.m_stop != True: + keyboard.wait('ctrl+alt') + if self.m_listening != True: + continue + + for i in range(3): + self.doCommand('up') + time.sleep(0.5) + + self.doCommand('left') + time.sleep(0.5) + + self.doCommand('right') + time.sleep(0.5) + + self.doCommand('stop') + + def stop(self): + self.m_stop = True + + def doAction(self, p_action): + # {'name': 'key action', 'key': 'left', 'type': 0} + # {'name': 'pause action', 'time': 0.03} + # {'name': 'command stop action', 'command name': 'down'} + # {'name': 'mouse move action', 'x':5, 'y':0, 'absolute': False} + # {'name': 'mouse click action', 'button': 'left', 'type': 0} + # {'name': 'mouse wheel action', 'delta':10} + w_actionName = p_action['name'] + if w_actionName == 'key action': + w_key = p_action['key'] + w_type = p_action['type'] + if w_type == 1: + keyboard.press(w_key) + print("pressed key: ", w_key) + elif w_type == 0: + keyboard.release(w_key) + print("released key: ", w_key) + elif w_type == 10: + keyboard.press(w_key) + keyboard.release(w_key) + print("pressed and released key: ", w_key) + elif w_actionName == 'pause action': + time.sleep(p_action['time']) + elif w_actionName == 'command stop action': + self.stopCommand(p_action['command name']) + elif w_actionName == 'mouse move action': + if p_action['absolute']: + ProfileExecutor.mouse.position([p_action['x'], p_action['y']]) + else: + ProfileExecutor.mouse.move(p_action['x'], p_action['y']) + elif w_actionName == 'mouse click action': + w_type = p_action['type'] + w_button = p_action['button'] + if w_type == 1: + if w_button == 'left': + ProfileExecutor.mouse.press(Button.left) + elif w_button == 'middle': + ProfileExecutor.mouse.press(Button.middle) + elif w_button == 'right': + ProfileExecutor.mouse.press(Button.right) + print("pressed mouse button: ", w_button) + elif w_type == 0: + if w_button == 'left': + ProfileExecutor.mouse.release(Button.left) + elif w_button == 'middle': + ProfileExecutor.mouse.release(Button.middle) + elif w_button == 'right': + ProfileExecutor.mouse.release(Button.right) + print("released mouse button: ", w_button) + elif w_type == 10: + if w_button == 'left': + ProfileExecutor.mouse.click(Button.left) + elif w_button == 'middle': + ProfileExecutor.mouse.click(Button.middle) + elif w_button == 'right': + ProfileExecutor.mouse.click(Button.right) + print("pressed and released mouse button: ", w_button) + elif w_actionName == 'mouse scroll action': + ProfileExecutor.mouse.scroll(0, p_action['delta']) + + class CommandThread(threading.Thread): + def __init__(self, p_ProfileExecutor, p_actions, p_repeat): + threading.Thread.__init__(self) + self.ProfileExecutor = p_ProfileExecutor + self.m_actions = p_actions + self.m_repeat = p_repeat + self.m_stop = False + def run(self): + w_repeat = self.m_repeat + while self.m_stop != True: + for w_action in self.m_actions: + self.ProfileExecutor.doAction(w_action) + w_repeat = w_repeat - 1 + if w_repeat == 0: + break + + def stop(self): + self.m_stop = True + threading.Thread.join(self) + + def doCommand(self, p_cmdName): + if self.m_profile == None: + return + + w_commands = self.m_profile['commands'] + flag = False + for w_command in w_commands: + if w_command['name'] == p_cmdName: + flag = True + break + if flag == False: + return + + w_actions = w_command['actions'] + w_async = w_command['async'] + + if w_async == False: + w_repeat = w_command['repeat'] + if w_repeat < 1: + w_repeat = 1 + while True: + for w_action in w_command['actions']: + self.doAction(w_action) + w_repeat = w_repeat - 1 + if w_repeat == 0: + break + else: + w_cmdThread = ProfileExecutor.CommandThread(self, w_actions, w_command['repeat']) + w_cmdThread.start() + self.m_cmdThreads[p_cmdName] = w_cmdThread + + def stopCommand(self, p_cmdName): + if p_cmdName in self.m_cmdThreads.keys(): + self.m_cmdThreads[p_cmdName].stop() + del self.m_cmdThreads[p_cmdName] + +# def carGameTest(): +# w_profileDict = { +# "name": "car game", +# "commands": [ +# {'name': 'up', +# 'actions': [ +# {'name': 'key action', 'key': 'up', 'type': 1}, +# {'name': 'pause action', 'time': 0.03} +# ], +# 'repeat': 1, +# 'async': False +# }, +# {'name': 'left', +# 'actions': [{'name': 'key action', 'key': 'right', 'type': 0}, +# {'name': 'pause action', 'time': 0.03}, +# {'name': 'key action', 'key': 'left', 'type': 1}, +# {'name': 'pause action', 'time': 0.03} +# ], +# 'repeat': 1, +# 'async': False +# }, +# {'name': 'right', +# 'actions': [{'name': 'key action', 'key': 'left', 'type': 0}, +# {'name': 'pause action', 'time': 0.03}, +# {'name': 'key action', 'key': 'right', 'type': 1}, +# {'name': 'pause action', 'time': 0.03} +# ], +# 'repeat': 1, +# 'async': False +# }, +# {'name': 'stop', +# 'actions': [ +# {'name': 'key action', 'key': 'left', 'type': 0}, +# {'name': 'pause action', 'time': 0.03}, +# {'name': 'key action', 'key': 'right', 'type': 0}, +# {'name': 'pause action', 'time': 0.03}, +# {'name': 'key action', 'key': 'up', 'type': 0}, +# {'name': 'pause action', 'time': 0.03} +# ], +# 'repeat': 1, +# 'async': False +# } +# ] +# } +# +# w_ProfileExecutor = ProfileExecutor(w_profileDict) +# +# print("Move to the target window and press spacebar") +# keyboard.wait('space') +# +# print("Started !") +# +# for i in range(5): +# w_ProfileExecutor.doCommand('up') +# time.sleep(0.5) +# +# w_ProfileExecutor.doCommand('left') +# time.sleep(0.5) +# +# w_ProfileExecutor.doCommand('right') +# time.sleep(0.5) +# +# w_ProfileExecutor.doCommand('stop') +# +# def airplaneGameTest(): +# w_profileDict = { +# "name": "airplane game", +# "commands": [ +# {'name': 'up', +# 'actions': [ +# {'name': 'command stop action', 'command name': 'down'}, +# {'name': 'mouse move action', 'x':0, 'y':-5, 'absolute': False}, +# {'name': 'pause action', 'time': 0.01} +# ], +# 'repeat': -1, +# 'async': True +# }, +# {'name': 'left', +# 'actions': [ +# {'name': 'command stop action', 'command name':'right'}, +# {'name': 'mouse move action', 'x':-5, 'y':0, 'absolute': False}, +# {'name': 'pause action', 'time': 0.005} +# ], +# 'repeat': -1, +# 'async': True +# }, +# {'name': 'right', +# 'actions': [ +# {'name': 'command stop action', 'command name': 'left'}, +# {'name': 'mouse move action', 'x':5, 'y':0, 'absolute': False}, +# {'name': 'pause action', 'time': 0.005} +# ], +# 'repeat': -1, +# 'async': True +# }, +# {'name': 'down', +# 'actions': [ +# {'name': 'command stop action', 'command name': 'up'}, +# {'name': 'mouse move action', 'x':0, 'y':5, 'absolute': False}, +# {'name': 'pause action', 'time': 0.005} +# ], +# 'repeat': -1, +# 'async': True +# }, +# {'name': 'shoot', +# 'actions': [ +# {'name': 'mouse click action', 'button':'left', 'type': 1}, +# {'name': 'pause action', 'time': 0.03} +# ], +# 'repeat': 1, +# 'async': False +# }, +# {'name': 'stop', +# 'actions': [ +# {'name': 'command stop action', 'command name': 'up'}, +# {'name': 'command stop action', 'command name': 'left'}, +# {'name': 'command stop action', 'command name': 'right'}, +# {'name': 'command stop action', 'command name': 'down'}, +# {'name': 'mouse click action', 'button': 'left', 'type': 0} +# ], +# 'repeat': 1, +# 'async': False +# } +# ] +# } +# +# w_ProfileExecutor = ProfileExecutor(w_profileDict) +# +# print("Move to the target window and press the spacebar to start") +# keyboard.wait('space') +# +# time.sleep(1) +# +# print("Started !") +# +# for i in range(3): +# w_ProfileExecutor.doCommand('shoot') +# time.sleep(1) +# +# w_ProfileExecutor.doCommand('up') +# time.sleep(1) +# +# w_ProfileExecutor.doCommand('down') +# time.sleep(0.5) +# +# w_ProfileExecutor.stopCommand('down') +# +# w_ProfileExecutor.doCommand('left') +# time.sleep(0.5) +# +# w_ProfileExecutor.doCommand('right') +# time.sleep(0.5) +# +# w_ProfileExecutor.doCommand('stop') +# +# if __name__ == "__main__": +# carGameTest() +# # airplaneGameTest() diff --git a/profiles.dat b/profiles.dat new file mode 100644 index 0000000..bbde7f6 Binary files /dev/null and b/profiles.dat differ diff --git a/pynput/__init__.py b/pynput/__init__.py new file mode 100644 index 0000000..9e8f5c6 --- /dev/null +++ b/pynput/__init__.py @@ -0,0 +1,41 @@ +# coding=utf-8 +# pynput +# Copyright (C) 2015-2018 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +The main *pynput* module. + +This module imports ``keyboard`` and ``mouse``. +""" + +def _logger(cls): + """Creates a logger with a name suitable for a specific class. + + This function takes into account that implementations for classes reside in + platform dependent modules, and thus removes the final part of the module + name. + + :param type cls: The class for which to create a logger. + + :return: a logger + """ + import logging + return logging.getLogger('{}.{}'.format( + '.'.join(cls.__module__.split('.', 2)[:2]), + cls.__name__)) + + +# from . import keyboard +from . import mouse diff --git a/pynput/__pycache__/__init__.cpython-36.pyc b/pynput/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000..930e817 Binary files /dev/null and b/pynput/__pycache__/__init__.cpython-36.pyc differ diff --git a/pynput/_info.py b/pynput/_info.py new file mode 100644 index 0000000..9f22b7d --- /dev/null +++ b/pynput/_info.py @@ -0,0 +1,19 @@ +# coding=utf-8 +# pystray +# Copyright (C) 2015-2018 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +__author__ = u'Moses Palmér' +__version__ = (1, 4, 2) diff --git a/pynput/_util/__init__.py b/pynput/_util/__init__.py new file mode 100644 index 0000000..3a28ad7 --- /dev/null +++ b/pynput/_util/__init__.py @@ -0,0 +1,296 @@ +# coding=utf-8 +# pynput +# Copyright (C) 2015-2018 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +General utility functions and classes. +""" + +# pylint: disable=R0903 +# We implement minimal mixins + +# pylint: disable=W0212 +# We implement an internal API + +import contextlib +import functools +import sys +import threading + +import six + +from six.moves import queue + + +class AbstractListener(threading.Thread): + """A class implementing the basic behaviour for event listeners. + + Instances of this class can be used as context managers. This is equivalent + to the following code:: + + listener.start() + listener.wait() + try: + with_statements() + finally: + listener.stop() + + Actual implementations of this class must set the attribute ``_log``, which + must be an instance of :class:`logging.Logger`. + + :param bool suppress: Whether to suppress events. Setting this to ``True`` + will prevent the input events from being passed to the rest of the + system. + + :param kwargs: A mapping from callback attribute to callback handler. All + handlers will be wrapped in a function reading the return value of the + callback, and if it ``is False``, raising :class:`StopException`. + + Any callback that is falsy will be ignored. + """ + class StopException(Exception): + """If an event listener callback raises this exception, the current + listener is stopped. + """ + pass + + #: Exceptions that are handled outside of the emitter and should thus not be + #: passed through the queue + _HANDLED_EXCEPTIONS = tuple() + + def __init__(self, suppress=False, **kwargs): + super(AbstractListener, self).__init__() + + def wrapper(f): + def inner(*args): + if f(*args) is False: + raise self.StopException() + return inner + + self._suppress = suppress + self._running = False + self._thread = threading.current_thread() + self._condition = threading.Condition() + self._ready = False + + # Allow multiple calls to stop + self._queue = queue.Queue(10) + + self.daemon = True + + for name, callback in kwargs.items(): + setattr(self, name, wrapper(callback or (lambda *a: None))) + + @property + def suppress(self): + """Whether to suppress events. + """ + return self._suppress + + @property + def running(self): + """Whether the listener is currently running. + """ + return self._running + + def stop(self): + """Stops listening for events. + + When this method returns, no more events will be delivered. + """ + if self._running: + self._running = False + self._queue.put(None) + self._stop_platform() + + def __enter__(self): + self.start() + self.wait() + return self + + def __exit__(self, exc_type, value, traceback): + self.stop() + + def wait(self): + """Waits for this listener to become ready. + """ + self._condition.acquire() + while not self._ready: + self._condition.wait() + self._condition.release() + + def run(self): + """The thread runner method. + """ + self._running = True + self._thread = threading.current_thread() + self._run() + + # Make sure that the queue contains something + self._queue.put(None) + + @classmethod + def _emitter(cls, f): + """A decorator to mark a method as the one emitting the callbacks. + + This decorator will wrap the method and catch exception. If a + :class:`StopException` is caught, the listener will be stopped + gracefully. If any other exception is caught, it will be propagated to + the thread calling :meth:`join` and reraised there. + """ + @functools.wraps(f) + def inner(self, *args, **kwargs): + # pylint: disable=W0702; we want to catch all exception + try: + return f(self, *args, **kwargs) + except Exception as e: + if not isinstance(e, self._HANDLED_EXCEPTIONS): + if not isinstance(e, AbstractListener.StopException): + self._log.exception( + 'Unhandled exception in listener callback') + self._queue.put( + None if isinstance(e, cls.StopException) + else sys.exc_info()) + self.stop() + raise + # pylint: enable=W0702 + + return inner + + def _mark_ready(self): + """Marks this listener as ready to receive events. + + This method must be called from :meth:`_run`. :meth:`wait` will block + until this method is called. + """ + self._condition.acquire() + self._ready = True + self._condition.notify() + self._condition.release() + + def _run(self): + """The implementation of the :meth:`run` method. + + This is a platform dependent implementation. + """ + raise NotImplementedError() + + def _stop_platform(self): + """The implementation of the :meth:`stop` method. + + This is a platform dependent implementation. + """ + raise NotImplementedError() + + def join(self, *args): + super(AbstractListener, self).join(*args) + + # Reraise any exceptions + try: + exc_type, exc_value, exc_traceback = self._queue.get() + except TypeError: + return + six.reraise(exc_type, exc_value, exc_traceback) + + +class NotifierMixin(object): + """A mixin for notifiers of fake events. + + This mixin can be used for controllers on platforms where sending fake + events does not cause a listener to receive a notification. + """ + def _emit(self, action, *args): + """Sends a notification to all registered listeners. + + This method will ensure that listeners that raise + :class:`StopException` are stopped. + + :param str action: The name of the notification. + + :param args: The arguments to pass. + """ + stopped = [] + for listener in self._listeners(): + try: + getattr(listener, action)(*args) + except listener.StopException: + stopped.append(listener) + for listener in stopped: + listener.stop() + + @classmethod + def _receiver(cls, listener_class): + """A decorator to make a class able to receive fake events from a + controller. + + This decorator will add the method ``_receive`` to the decorated class. + + This method is a context manager which ensures that all calls to + :meth:`_emit` will invoke the named method in the listener instance + while the block is active. + """ + @contextlib.contextmanager + def receive(self): + """Executes a code block with this listener instance registered as + a receiver of fake input events. + """ + self._controller_class._add_listener(self) + try: + yield + finally: + self._controller_class._remove_listener(self) + + listener_class._receive = receive + listener_class._controller_class = cls + + # Make sure this class has the necessary attributes + if not hasattr(cls, '_listener_cache'): + cls._listener_cache = set() + cls._listener_lock = threading.Lock() + + return listener_class + + @classmethod + def _listeners(cls): + """Iterates over the set of running listeners. + + This method will quit without acquiring the lock if the set is empty, + so there is potential for race conditions. This is an optimisation, + since :class:`Controller` will need to call this method for every + control event. + """ + if not cls._listener_cache: + return + with cls._listener_lock: + for listener in cls._listener_cache: + yield listener + + @classmethod + def _add_listener(cls, listener): + """Adds a listener to the set of running listeners. + + :param listener: The listener for fake events. + """ + with cls._listener_lock: + cls._listener_cache.add(listener) + + @classmethod + def _remove_listener(cls, listener): + """Removes this listener from the set of running listeners. + + :param listener: The listener for fake events. + """ + with cls._listener_lock: + cls._listener_cache.remove(listener) diff --git a/pynput/_util/__pycache__/__init__.cpython-36.pyc b/pynput/_util/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000..552454f Binary files /dev/null and b/pynput/_util/__pycache__/__init__.cpython-36.pyc differ diff --git a/pynput/_util/__pycache__/win32.cpython-36.pyc b/pynput/_util/__pycache__/win32.cpython-36.pyc new file mode 100644 index 0000000..ebe3a90 Binary files /dev/null and b/pynput/_util/__pycache__/win32.cpython-36.pyc differ diff --git a/pynput/_util/__pycache__/win32_vks.cpython-36.pyc b/pynput/_util/__pycache__/win32_vks.cpython-36.pyc new file mode 100644 index 0000000..25fc789 Binary files /dev/null and b/pynput/_util/__pycache__/win32_vks.cpython-36.pyc differ diff --git a/pynput/_util/__pycache__/xorg.cpython-36.pyc b/pynput/_util/__pycache__/xorg.cpython-36.pyc new file mode 100644 index 0000000..4c58543 Binary files /dev/null and b/pynput/_util/__pycache__/xorg.cpython-36.pyc differ diff --git a/pynput/_util/__pycache__/xorg_keysyms.cpython-36.pyc b/pynput/_util/__pycache__/xorg_keysyms.cpython-36.pyc new file mode 100644 index 0000000..5aabe76 Binary files /dev/null and b/pynput/_util/__pycache__/xorg_keysyms.cpython-36.pyc differ diff --git a/pynput/_util/darwin.py b/pynput/_util/darwin.py new file mode 100644 index 0000000..853959a --- /dev/null +++ b/pynput/_util/darwin.py @@ -0,0 +1,273 @@ +# coding=utf-8 +# pynput +# Copyright (C) 2015-2018 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +Utility functions and classes for the *Darwin* backend. +""" + +# pylint: disable=C0103 +# pylint: disable=R0903 +# This module contains wrapper classes + +import contextlib +import ctypes +import ctypes.util +import six + +import objc +import CoreFoundation +import Quartz + +from . import AbstractListener + + +#: The objc module as a library handle +OBJC = ctypes.PyDLL(objc._objc.__file__) + +OBJC.PyObjCObject_New.restype = ctypes.py_object +OBJC.PyObjCObject_New.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int] + + +def _wrap_value(value): + """Converts a pointer to a *Python objc* value. + + :param value: The pointer to convert. + + :return: a wrapped value + """ + return OBJC.PyObjCObject_New(value, 0, 1) + + +@contextlib.contextmanager +def _wrapped(value): + """A context manager that converts a raw pointer to a *Python objc* value. + + When the block is exited, the value is released. + + :param value: The raw value to wrap. + """ + wrapped_value = _wrap_value(value) + + try: + yield value + finally: + CoreFoundation.CFRelease(wrapped_value) + + +class CarbonExtra(object): + """A class exposing some missing functionality from *Carbon* as class + attributes. + """ + _Carbon = ctypes.cdll.LoadLibrary(ctypes.util.find_library('Carbon')) + + _Carbon.TISCopyCurrentKeyboardInputSource.argtypes = [] + _Carbon.TISCopyCurrentKeyboardInputSource.restype = ctypes.c_void_p + + _Carbon.TISCopyCurrentASCIICapableKeyboardLayoutInputSource.argtypes = [] + _Carbon.TISCopyCurrentASCIICapableKeyboardLayoutInputSource.restype = \ + ctypes.c_void_p + + _Carbon.TISGetInputSourceProperty.argtypes = [ + ctypes.c_void_p, ctypes.c_void_p] + _Carbon.TISGetInputSourceProperty.restype = ctypes.c_void_p + + _Carbon.LMGetKbdType.argtypes = [] + _Carbon.LMGetKbdType.restype = ctypes.c_uint32 + + _Carbon.UCKeyTranslate.argtypes = [ + ctypes.c_void_p, + ctypes.c_uint16, + ctypes.c_uint16, + ctypes.c_uint32, + ctypes.c_uint32, + ctypes.c_uint32, + ctypes.POINTER(ctypes.c_uint32), + ctypes.c_uint8, + ctypes.POINTER(ctypes.c_uint8), + ctypes.c_uint16 * 4] + _Carbon.UCKeyTranslate.restype = ctypes.c_uint32 + + TISCopyCurrentKeyboardInputSource = \ + _Carbon.TISCopyCurrentKeyboardInputSource + + TISCopyCurrentASCIICapableKeyboardLayoutInputSource = \ + _Carbon.TISCopyCurrentASCIICapableKeyboardLayoutInputSource + + kTISPropertyUnicodeKeyLayoutData = ctypes.c_void_p.in_dll( + _Carbon, 'kTISPropertyUnicodeKeyLayoutData') + + TISGetInputSourceProperty = \ + _Carbon.TISGetInputSourceProperty + + LMGetKbdType = \ + _Carbon.LMGetKbdType + + kUCKeyActionDisplay = 3 + kUCKeyTranslateNoDeadKeysBit = 0 + + UCKeyTranslate = \ + _Carbon.UCKeyTranslate + + +@contextlib.contextmanager +def keycode_context(): + """Returns an opaque value representing a context for translating keycodes + to strings. + """ + keyboard_type, layout_data = None, None + for source in [ + CarbonExtra.TISCopyCurrentKeyboardInputSource, + CarbonExtra.TISCopyCurrentASCIICapableKeyboardLayoutInputSource]: + with _wrapped(source()) as keyboard: + keyboard_type = CarbonExtra.LMGetKbdType() + layout = _wrap_value(CarbonExtra.TISGetInputSourceProperty( + keyboard, + CarbonExtra.kTISPropertyUnicodeKeyLayoutData)) + layout_data = layout.bytes().tobytes() if layout else None + if keyboard is not None and layout_data is not None: + break + yield (keyboard_type, layout_data) + + +def keycode_to_string(context, keycode, modifier_state=0): + """Converts a keycode to a string. + """ + LENGTH = 4 + + keyboard_type, layout_data = context + + dead_key_state = ctypes.c_uint32() + length = ctypes.c_uint8() + unicode_string = (ctypes.c_uint16 * LENGTH)() + CarbonExtra.UCKeyTranslate( + layout_data, + keycode, + CarbonExtra.kUCKeyActionDisplay, + modifier_state, + keyboard_type, + CarbonExtra.kUCKeyTranslateNoDeadKeysBit, + ctypes.byref(dead_key_state), + LENGTH, + ctypes.byref(length), + unicode_string) + return u''.join( + six.unichr(unicode_string[i]) + for i in range(length.value)) + + +def get_unicode_to_keycode_map(): + """Returns a mapping from unicode strings to virtual key codes. + + :return: a dict mapping key codes to strings + """ + with keycode_context() as context: + return { + keycode_to_string(context, keycode): keycode + for keycode in range(128)} + + +class ListenerMixin(object): + """A mixin for *Quartz* event listeners. + + Subclasses should set a value for :attr:`_EVENTS` and implement + :meth:`_handle`. + """ + #: The events that we listen to + _EVENTS = tuple() + + def _run(self): + self._loop = None + try: + tap = self._create_event_tap() + if tap is None: + self._mark_ready() + return + + loop_source = Quartz.CFMachPortCreateRunLoopSource( + None, tap, 0) + self._loop = Quartz.CFRunLoopGetCurrent() + + Quartz.CFRunLoopAddSource( + self._loop, loop_source, Quartz.kCFRunLoopDefaultMode) + Quartz.CGEventTapEnable(tap, True) + + self._mark_ready() + + # pylint: disable=W0702; we want to silence errors + try: + while self.running: + result = Quartz.CFRunLoopRunInMode( + Quartz.kCFRunLoopDefaultMode, 1, False) + try: + if result != Quartz.kCFRunLoopRunTimedOut: + break + except AttributeError: + # This happens during teardown of the virtual machine + break + + except: + # This exception will have been passed to the main thread + pass + # pylint: enable=W0702 + + finally: + self._loop = None + + def _stop_platform(self): + # The base class sets the running flag to False; this will cause the + # loop around run loop invocations to terminate and set this event + try: + if self._loop is not None: + Quartz.CFRunLoopStop(self._loop) + except AttributeError: + # The loop may not have been created + pass + + def _create_event_tap(self): + """Creates the event tap used by the listener. + + :return: an event tap + """ + return Quartz.CGEventTapCreate( + Quartz.kCGSessionEventTap, + Quartz.kCGHeadInsertEventTap, + Quartz.kCGEventTapOptionListenOnly if (True + and not self.suppress + and self._intercept is None) + else Quartz.kCGEventTapOptionDefault, + self._EVENTS, + self._handler, + None) + + @AbstractListener._emitter + def _handler(self, proxy, event_type, event, refcon): + """The callback registered with *Mac OSX* for mouse events. + + This method will call the callbacks registered on initialisation. + """ + self._handle(proxy, event_type, event, refcon) + if self._intercept is not None: + return self._intercept(event_type, event) + elif self.suppress: + return None + + def _handle(self, proxy, event_type, event, refcon): + """The device specific callback handler. + + This method calls the appropriate callback registered when this + listener was created based on the event. + """ + raise NotImplementedError() diff --git a/pynput/_util/win32.py b/pynput/_util/win32.py new file mode 100644 index 0000000..a1a4852 --- /dev/null +++ b/pynput/_util/win32.py @@ -0,0 +1,641 @@ +# coding=utf-8 +# pynput +# Copyright (C) 2015-2018 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +Utility functions and classes for the *win32* backend. +""" + +# pylint: disable=C0103 +# We want to make it obvious how structs are related + +# pylint: disable=R0903 +# This module contains a number of structs + +import contextlib +import ctypes +import threading + +from ctypes import ( + windll, + wintypes) + +import six + +from . import AbstractListener + + +# LPDWORD is not in ctypes.wintypes on Python 2 +if not hasattr(wintypes, 'LPDWORD'): + wintypes.LPDWORD = ctypes.POINTER(wintypes.DWORD) + + +class MOUSEINPUT(ctypes.Structure): + """Contains information about a simulated mouse event. + """ + MOVE = 0x0001 + LEFTDOWN = 0x0002 + LEFTUP = 0x0004 + RIGHTDOWN = 0x0008 + RIGHTUP = 0x0010 + MIDDLEDOWN = 0x0020 + MIDDLEUP = 0x0040 + XDOWN = 0x0080 + XUP = 0x0100 + WHEEL = 0x0800 + HWHEEL = 0x1000 + ABSOLUTE = 0x8000 + + XBUTTON1 = 0x0001 + XBUTTON2 = 0x0002 + + _fields_ = [ + ('dx', wintypes.LONG), + ('dy', wintypes.LONG), + ('mouseData', wintypes.DWORD), + ('dwFlags', wintypes.DWORD), + ('time', wintypes.DWORD), + ('dwExtraInfo', ctypes.c_void_p)] + + +class KEYBDINPUT(ctypes.Structure): + """Contains information about a simulated keyboard event. + """ + EXTENDEDKEY = 0x0001 + KEYUP = 0x0002 + SCANCODE = 0x0008 + UNICODE = 0x0004 + + _fields_ = [ + ('wVk', wintypes.WORD), + ('wScan', wintypes.WORD), + ('dwFlags', wintypes.DWORD), + ('time', wintypes.DWORD), + ('dwExtraInfo', ctypes.c_void_p)] + + +class HARDWAREINPUT(ctypes.Structure): + """Contains information about a simulated message generated by an input + device other than a keyboard or mouse. + """ + _fields_ = [ + ('uMsg', wintypes.DWORD), + ('wParamL', wintypes.WORD), + ('wParamH', wintypes.WORD)] + + +class INPUT_union(ctypes.Union): + """Represents the union of input types in :class:`INPUT`. + """ + _fields_ = [ + ('mi', MOUSEINPUT), + ('ki', KEYBDINPUT), + ('hi', HARDWAREINPUT)] + + +class INPUT(ctypes.Structure): + """Used by :attr:`SendInput` to store information for synthesizing input + events such as keystrokes, mouse movement, and mouse clicks. + """ + MOUSE = 0 + KEYBOARD = 1 + HARDWARE = 2 + + _fields_ = [ + ('type', wintypes.DWORD), + ('value', INPUT_union)] + + +LPINPUT = ctypes.POINTER(INPUT) + +VkKeyScan = windll.user32.VkKeyScanW +VkKeyScan.argtypes = ( + wintypes.WCHAR,) + +SendInput = windll.user32.SendInput +SendInput.argtypes = ( + wintypes.UINT, + LPINPUT, + ctypes.c_int) + +GetCurrentThreadId = windll.kernel32.GetCurrentThreadId +GetCurrentThreadId.restype = wintypes.DWORD + + +class MessageLoop(object): + """A class representing a message loop. + """ + #: The message that signals this loop to terminate + WM_STOP = 0x0401 + + _LPMSG = ctypes.POINTER(wintypes.MSG) + + _GetMessage = windll.user32.GetMessageW + _GetMessage.argtypes = ( + _LPMSG, + wintypes.HWND, + wintypes.UINT, + wintypes.UINT) + _PeekMessage = windll.user32.PeekMessageW + _PeekMessage.argtypes = ( + _LPMSG, + wintypes.HWND, + wintypes.UINT, + wintypes.UINT, + wintypes.UINT) + _PostThreadMessage = windll.user32.PostThreadMessageW + _PostThreadMessage.argtypes = ( + wintypes.DWORD, + wintypes.UINT, + wintypes.WPARAM, + wintypes.LPARAM) + + PM_NOREMOVE = 0 + + def __init__(self): + self._threadid = None + self._event = threading.Event() + self.thread = None + + def __iter__(self): + """Initialises the message loop and yields all messages until + :meth:`stop` is called. + + :raises AssertionError: if :meth:`start` has not been called + """ + assert self._threadid is not None + + try: + # Pump messages until WM_STOP + while True: + msg = wintypes.MSG() + lpmsg = ctypes.byref(msg) + r = self._GetMessage(lpmsg, None, 0, 0) + if r <= 0 or msg.message == self.WM_STOP: + break + else: + yield msg + + finally: + self._threadid = None + self.thread = None + + def start(self): + """Starts the message loop. + + This method must be called before iterating over messages, and it must + be called from the same thread. + """ + self._threadid = GetCurrentThreadId() + self.thread = threading.current_thread() + + # Create the message loop + msg = wintypes.MSG() + lpmsg = ctypes.byref(msg) + self._PeekMessage(lpmsg, None, 0x0400, 0x0400, self.PM_NOREMOVE) + + # Set the event to signal to other threads that the loop is created + self._event.set() + + def stop(self): + """Stops the message loop. + """ + self._event.wait() + if self._threadid: + self.post(self.WM_STOP, 0, 0) + + def post(self, msg, wparam, lparam): + """Posts a message to this message loop. + + :param ctypes.wintypes.UINT msg: The message. + + :param ctypes.wintypes.WPARAM wparam: The value of ``wParam``. + + :param ctypes.wintypes.LPARAM lparam: The value of ``lParam``. + """ + self._PostThreadMessage(self._threadid, msg, wparam, lparam) + + +class SystemHook(object): + """A class to handle Windows hooks. + """ + #: The hook action value for actions we should check + HC_ACTION = 0 + + _HOOKPROC = ctypes.WINFUNCTYPE( + wintypes.LPARAM, + ctypes.c_int32, wintypes.WPARAM, wintypes.LPARAM) + + _SetWindowsHookEx = windll.user32.SetWindowsHookExW + _SetWindowsHookEx.argtypes = ( + ctypes.c_int, + _HOOKPROC, + wintypes.HINSTANCE, + wintypes.DWORD) + _UnhookWindowsHookEx = windll.user32.UnhookWindowsHookEx + _UnhookWindowsHookEx.argtypes = ( + wintypes.HHOOK,) + _CallNextHookEx = windll.user32.CallNextHookEx + _CallNextHookEx.argtypes = ( + wintypes.HHOOK, + ctypes.c_int, + wintypes.WPARAM, + wintypes.LPARAM) + + #: The registered hook procedures + _HOOKS = {} + + class SuppressException(Exception): + """An exception raised by a hook callback to suppress further + propagation of events. + """ + pass + + def __init__(self, hook_id, on_hook=lambda code, msg, lpdata: None): + self.hook_id = hook_id + self.on_hook = on_hook + self._hook = None + + def __enter__(self): + key = threading.current_thread().ident + assert key not in self._HOOKS + + # Add ourself to lookup table and install the hook + self._HOOKS[key] = self + self._hook = self._SetWindowsHookEx( + self.hook_id, + self._handler, + None, + 0) + + return self + + def __exit__(self, exc_type, value, traceback): + key = threading.current_thread().ident + assert key in self._HOOKS + + if self._hook is not None: + # Uninstall the hook and remove ourself from lookup table + self._UnhookWindowsHookEx(self._hook) + del self._HOOKS[key] + + @staticmethod + @_HOOKPROC + def _handler(code, msg, lpdata): + key = threading.current_thread().ident + self = SystemHook._HOOKS.get(key, None) + if self: + # pylint: disable=W0702; we want to silence errors + try: + self.on_hook(code, msg, lpdata) + except self.SuppressException: + # Return non-zero to stop event propagation + return 1 + except: + # Ignore any errors + pass + # pylint: enable=W0702 + return SystemHook._CallNextHookEx(0, code, msg, lpdata) + + +class ListenerMixin(object): + """A mixin for *win32* event listeners. + + Subclasses should set a value for :attr:`_EVENTS` and implement + :meth:`_handle`. + + Subclasses must also be decorated with a decorator compatible with + :meth:`pynput._util.NotifierMixin._receiver` or implement the method + ``_receive()``. + """ + #: The Windows hook ID for the events to capture + _EVENTS = None + + #: The window message used to signal that an even should be handled + _WM_PROCESS = 0x410 + + def suppress_event(self): + """Causes the currently filtered event to be suppressed. + + This has a system wide effect and will generally result in no + applications receiving the event. + + This method will raise an undefined exception. + """ + raise SystemHook.SuppressException() + + def _run(self): + self._message_loop = MessageLoop() + with self._receive(): + self._mark_ready() + self._message_loop.start() + + # pylint: disable=W0702; we want to silence errors + try: + with SystemHook(self._EVENTS, self._handler): + # Just pump messages + for msg in self._message_loop: + if not self.running: + break + if msg.message == self._WM_PROCESS: + self._process(msg.wParam, msg.lParam) + except: + # This exception will have been passed to the main thread + pass + # pylint: enable=W0702 + + def _stop_platform(self): + try: + self._message_loop.stop() + except AttributeError: + # The loop may not have been created + pass + + @AbstractListener._emitter + def _handler(self, code, msg, lpdata): + """The callback registered with *Windows* for events. + + This method will post the message :attr:`_WM_HANDLE` to the message loop + started with this listener using :meth:`MessageLoop.post`. The + parameters are retrieved with a call to :meth:`_handle`. + """ + try: + converted = self._convert(code, msg, lpdata) + if converted is not None: + self._message_loop.post(self._WM_PROCESS, *converted) + except NotImplementedError: + self._handle(code, msg, lpdata) + + if self.suppress: + self.suppress_event() + + def _convert(self, code, msg, lpdata): + """The device specific callback handler. + + This method converts a low-level message and data to a + ``WPARAM`` / ``LPARAM`` pair. + """ + raise NotImplementedError() + + def _process(self, wparam, lparam): + """The device specific callback handler. + + This method performs the actual dispatching of events. + """ + raise NotImplementedError() + + def _handle(self, code, msg, lpdata): + """The device specific callback handler. + + This method calls the appropriate callback registered when this + listener was created based on the event. + + This method is only called if :meth:`_convert` is not implemented. + """ + raise NotImplementedError() + + +class KeyTranslator(object): + """A class to translate virtual key codes to characters. + """ + _AttachThreadInput = ctypes.windll.user32.AttachThreadInput + _AttachThreadInput.argtypes = ( + wintypes.DWORD, + wintypes.DWORD, + wintypes.BOOL) + _GetForegroundWindow = ctypes.windll.user32.GetForegroundWindow + _GetKeyboardLayout = ctypes.windll.user32.GetKeyboardLayout + _GetKeyboardLayout.argtypes = ( + wintypes.DWORD,) + _GetKeyboardState = ctypes.windll.user32.GetKeyboardState + _GetKeyboardState.argtypes = ( + ctypes.c_voidp,) + _GetWindowThreadProcessId = ctypes.windll.user32.GetWindowThreadProcessId + _GetWindowThreadProcessId.argtypes = ( + wintypes.HWND, + wintypes.LPDWORD) + _MapVirtualKeyEx = ctypes.windll.user32.MapVirtualKeyExW + _MapVirtualKeyEx.argtypes = ( + wintypes.UINT, + wintypes.UINT, + wintypes.HKL) + _ToUnicodeEx = ctypes.windll.user32.ToUnicodeEx + _ToUnicodeEx.argtypes = ( + wintypes.UINT, + wintypes.UINT, + ctypes.c_voidp, + ctypes.c_voidp, + ctypes.c_int, + wintypes.UINT, + wintypes.HKL) + + _MAPVK_VK_TO_VSC = 0 + _MAPVK_VK_TO_CHAR = 2 + + def __init__(self): + self.__state = (ctypes.c_ubyte * 255)() + self._cache = {} + self._reinject_arguments = None + + def __call__(self, vk, is_press): + """Converts a virtual key code to a string. + + :param int vk: The virtual key code. + + :param bool is_press: Whether this is a press. Because the *win32* + functions used to translate the key modifies internal kernel state, + some cleanup must be performed for key presses. + + :return: parameters suitable for the :class:`pynput.keyboard.KeyCode` + constructor + + :raises OSError: if a call to any *win32* function fails + """ + # Get the keyboard state and layout + state, layout = self._get_state_and_layout() + + # Get the scan code for the virtual key + scan = self._to_scan(vk, layout) + + # Try to reuse the previous key in the cache + try: + if is_press: + return self._cache[vk] + else: + return self._cache.pop(vk) + except KeyError: + pass + + # Get a string representation of the key + char, is_dead = self._to_char(vk, layout) + modified_char = self._to_char_with_modifiers(vk, layout, scan, state) + + # Clear the keyboard state if the key was a dead key + if is_dead: + self._reset_state(vk, layout, scan) + + # If the previous key handled was a dead key, we reinject it + if self._reinject_arguments: + self._reinject(*self._reinject_arguments) + self._reinject_arguments = None + + # If the current key is a dead key, we store the current state to be + # able to reinject later + elif is_dead: + self._reinject_arguments = ( + vk, + layout, + scan, + (ctypes.c_ubyte * 255)(*state)) + + # Otherwise we just clear any previous dead key state + else: + self._reinject_arguments = None + + # Update the cache + self._cache[vk] = { + 'char': modified_char or char, + 'is_dead': is_dead, + 'vk': vk} + + return self._cache[vk] + + def _get_state_and_layout(self): + """Returns the keyboard state and layout. + + The state is read from the currently active window if possible. It is + kept in a cache, so any call to this method will invalidate return + values from previous invocations. + + :return: the tuple ``(state, layout)`` + """ + # Get the state of the keyboard attached to the active window + with self._thread_input() as active_thread: + if not self._GetKeyboardState(ctypes.byref(self.__state)): + raise OSError( + 'GetKeyboardState failed: %d', + ctypes.wintypes.get_last_error()) + + # Get the keyboard layout for the thread for which we retrieved the + # state + layout = self._GetKeyboardLayout(active_thread) + + return (self.__state, layout) + + def _to_scan(self, vk, layout): + """Retrieves the scan code for a virtual key code. + + :param int vk: The virtual key code. + + :param layout: The keyboard layout. + + :return: the scan code + """ + return self._MapVirtualKeyEx( + vk, self._MAPVK_VK_TO_VSC, layout) + + def _to_char(self, vk, layout): + """Converts a virtual key by simply mapping it through the keyboard + layout. + + This method is stateless, so any active shift state or dead keys are + ignored. + + :param int vk: The virtual key code. + + :param layout: The keyboard layout. + + :return: the string representation of the key, or ``None``, and whether + was dead as the tuple ``(char, is_dead)`` + """ + # MapVirtualKeyEx will yield a string representation for dead keys + flags_and_codepoint = self._MapVirtualKeyEx( + vk, self._MAPVK_VK_TO_CHAR, layout) + if flags_and_codepoint: + return ( + six.unichr(flags_and_codepoint & 0xFFFF), + bool(flags_and_codepoint & (1 << 31))) + else: + return (None, None) + + def _to_char_with_modifiers(self, vk, layout, scan, state): + """Converts a virtual key by mapping it through the keyboard layout and + internal kernel keyboard state. + + This method is stateful, so any active shift state and dead keys are + applied. Currently active dead keys will be removed from the internal + kernel keyboard state. + + :param int vk: The virtual key code. + + :param layout: The keyboard layout. + + :param int scan: The scan code of the key. + + :param state: The keyboard state. + + :return: the string representation of the key, or ``None`` + """ + # This will apply any dead keys and modify the internal kernel keyboard + # state + out = (ctypes.wintypes.WCHAR * 5)() + count = self._ToUnicodeEx( + vk, scan, ctypes.byref(state), ctypes.byref(out), + len(out), 0, layout) + + return out[0] if count > 0 else None + + def _reset_state(self, vk, layout, scan): + """Clears the internal kernel keyboard state. + + This method will remove all dead keys from the internal state. + + :param int vk: The virtual key code. + + :param layout: The keyboard layout. + + :param int scan: The scan code of the key. + """ + state = (ctypes.c_byte * 255)() + out = (ctypes.wintypes.WCHAR * 5)() + while self._ToUnicodeEx( + vk, scan, ctypes.byref(state), ctypes.byref(out), + len(out), 0, layout) < 0: + pass + + def _reinject(self, vk, layout, scan, state): + """Reinjects the previous dead key. + + This must be called if ``ToUnicodeEx`` has been called, and the + previous key was a dead one. + + :param int vk: The virtual key code. + + :param layout: The keyboard layout. + + :param int scan: The scan code of the key. + + :param state: The keyboard state. + """ + out = (ctypes.wintypes.WCHAR * 5)() + self._ToUnicodeEx( + vk, scan, ctypes.byref(state), ctypes.byref(out), + len(out), 0, layout) + + @contextlib.contextmanager + def _thread_input(self): + """Yields the current thread ID. + """ + yield GetCurrentThreadId() diff --git a/pynput/_util/win32_vks.py b/pynput/_util/win32_vks.py new file mode 100644 index 0000000..8d50c73 --- /dev/null +++ b/pynput/_util/win32_vks.py @@ -0,0 +1,179 @@ +# coding: utf-8 +# pynput +# Copyright (C) 2015-2018 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +# pylint: disable=C0111,C0302 + +LBUTTON = 1 +RBUTTON = 2 +CANCEL = 3 +MBUTTON = 4 +XBUTTON1 = 5 +XBUTTON2 = 6 +BACK = 8 +TAB = 9 +CLEAR = 12 +RETURN = 13 +SHIFT = 16 +CONTROL = 17 +MENU = 18 +PAUSE = 19 +CAPITAL = 20 +KANA = 21 +HANGEUL = 21 +HANGUL = 21 +JUNJA = 23 +FINAL = 24 +HANJA = 25 +KANJI = 25 +ESCAPE = 27 +CONVERT = 28 +NONCONVERT = 29 +ACCEPT = 30 +MODECHANGE = 31 +SPACE = 32 +PRIOR = 33 +NEXT = 34 +END = 35 +HOME = 36 +LEFT = 37 +UP = 38 +RIGHT = 39 +DOWN = 40 +SELECT = 41 +PRINT = 42 +EXECUTE = 43 +SNAPSHOT = 44 +INSERT = 45 +DELETE = 46 +HELP = 47 +LWIN = 91 +RWIN = 92 +APPS = 93 +SLEEP = 95 +NUMPAD0 = 96 +NUMPAD1 = 97 +NUMPAD2 = 98 +NUMPAD3 = 99 +NUMPAD4 = 100 +NUMPAD5 = 101 +NUMPAD6 = 102 +NUMPAD7 = 103 +NUMPAD8 = 104 +NUMPAD9 = 105 +MULTIPLY = 106 +ADD = 107 +SEPARATOR = 108 +SUBTRACT = 109 +DECIMAL = 110 +DIVIDE = 111 +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 +NUMLOCK = 144 +SCROLL = 145 +OEM_NEC_EQUAL = 146 +OEM_FJ_JISHO = 146 +OEM_FJ_MASSHOU = 147 +OEM_FJ_TOUROKU = 148 +OEM_FJ_LOYA = 149 +OEM_FJ_ROYA = 150 +LSHIFT = 160 +RSHIFT = 161 +LCONTROL = 162 +RCONTROL = 163 +LMENU = 164 +RMENU = 165 +BROWSER_BACK = 166 +BROWSER_FORWARD = 167 +BROWSER_REFRESH = 168 +BROWSER_STOP = 169 +BROWSER_SEARCH = 170 +BROWSER_FAVORITES = 171 +BROWSER_HOME = 172 +VOLUME_MUTE = 173 +VOLUME_DOWN = 174 +VOLUME_UP = 175 +MEDIA_NEXT_TRACK = 176 +MEDIA_PREV_TRACK = 177 +MEDIA_STOP = 178 +MEDIA_PLAY_PAUSE = 179 +LAUNCH_MAIL = 180 +LAUNCH_MEDIA_SELECT = 181 +LAUNCH_APP1 = 182 +LAUNCH_APP2 = 183 +OEM_1 = 186 +OEM_PLUS = 187 +OEM_COMMA = 188 +OEM_MINUS = 189 +OEM_PERIOD = 190 +OEM_2 = 191 +OEM_3 = 192 +OEM_4 = 219 +OEM_5 = 220 +OEM_6 = 221 +OEM_7 = 222 +OEM_8 = 223 +OEM_AX = 225 +OEM_102 = 226 +ICO_HELP = 227 +ICO_00 = 228 +PROCESSKEY = 229 +ICO_CLEAR = 230 +PACKET = 231 +OEM_RESET = 233 +OEM_JUMP = 234 +OEM_PA1 = 235 +OEM_PA2 = 236 +OEM_PA3 = 237 +OEM_WSCTRL = 238 +OEM_CUSEL = 239 +OEM_ATTN = 240 +OEM_FINISH = 241 +OEM_COPY = 242 +OEM_AUTO = 243 +OEM_ENLW = 244 +OEM_BACKTAB = 245 +ATTN = 246 +CRSEL = 247 +EXSEL = 248 +EREOF = 249 +PLAY = 250 +ZOOM = 251 +NONAME = 252 +PA1 = 253 +OEM_CLEAR = 254 diff --git a/pynput/_util/xorg.py b/pynput/_util/xorg.py new file mode 100644 index 0000000..a089d0a --- /dev/null +++ b/pynput/_util/xorg.py @@ -0,0 +1,480 @@ +# coding=utf-8 +# pynput +# Copyright (C) 2015-2018 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +Utility functions and classes for the *Xorg* backend. +""" + +# pylint: disable=R0903 +# We implement stubs + +import contextlib +import functools +import itertools +import operator +import Xlib.display +import Xlib.threaded +import Xlib.XK + +from . import AbstractListener +from .xorg_keysyms import SYMBOLS + + +# Create a display to verify that we have an X connection +def _check(): + display = Xlib.display.Display() + display.close() +_check() +del _check + + +class X11Error(Exception): + """An error that is thrown at the end of a code block managed by a + :func:`display_manager` if an *X11* error occurred. + """ + pass + + +@contextlib.contextmanager +def display_manager(display): + """Traps *X* errors and raises an :class:``X11Error`` at the end if any + error occurred. + + This handler also ensures that the :class:`Xlib.display.Display` being + managed is sync'd. + + :param Xlib.display.Display display: The *X* display. + + :return: the display + :rtype: Xlib.display.Display + """ + errors = [] + + def handler(*args): + """The *Xlib* error handler. + """ + errors.append(args) + + old_handler = display.set_error_handler(handler) + try: + yield display + display.sync() + finally: + display.set_error_handler(old_handler) + if errors: + raise X11Error(errors) + + +def _find_mask(display, symbol): + """Returns the mode flags to use for a modifier symbol. + + :param Xlib.display.Display display: The *X* display. + + :param str symbol: The name of the symbol. + + :return: the modifier mask + """ + # Get the key code for the symbol + modifier_keycode = display.keysym_to_keycode( + Xlib.XK.string_to_keysym(symbol)) + + for index, keycodes in enumerate(display.get_modifier_mapping()): + for keycode in keycodes: + if keycode == modifier_keycode: + return 1 << index + + return 0 + + +def alt_mask(display): + """Returns the *alt* mask flags. + + The first time this function is called for a display, the value is cached. + Subsequent calls will return the cached value. + + :param Xlib.display.Display display: The *X* display. + + :return: the modifier mask + """ + if not hasattr(display, '__alt_mask'): + display.__alt_mask = _find_mask(display, 'Alt_L') + return display.__alt_mask + + +def alt_gr_mask(display): + """Returns the *alt* mask flags. + + The first time this function is called for a display, the value is cached. + Subsequent calls will return the cached value. + + :param Xlib.display.Display display: The *X* display. + + :return: the modifier mask + """ + if not hasattr(display, '__altgr_mask'): + display.__altgr_mask = _find_mask(display, 'Mode_switch') + return display.__altgr_mask + + +def numlock_mask(display): + """Returns the *numlock* mask flags. + + The first time this function is called for a display, the value is cached. + Subsequent calls will return the cached value. + + :param Xlib.display.Display display: The *X* display. + + :return: the modifier mask + """ + if not hasattr(display, '__numlock_mask'): + display.__numlock_mask = _find_mask(display, 'Num_Lock') + return display.__numlock_mask + + +def keysym_is_latin_upper(keysym): + """Determines whether a *keysym* is an upper case *latin* character. + + This is true only if ``XK_A`` <= ``keysym`` <= ` XK_Z``. + + :param in keysym: The *keysym* to check. + """ + return Xlib.XK.XK_A <= keysym <= Xlib.XK.XK_Z + + +def keysym_is_latin_lower(keysym): + """Determines whether a *keysym* is a lower case *latin* character. + + This is true only if ``XK_a`` <= ``keysym`` <= ` XK_z``. + + :param in keysym: The *keysym* to check. + """ + return Xlib.XK.XK_a <= keysym <= Xlib.XK.XK_z + + +def keysym_group(ks1, ks2): + """Generates a group from two *keysyms*. + + The implementation of this function comes from: + + Within each group, if the second element of the group is ``NoSymbol``, + then the group should be treated as if the second element were the same + as the first element, except when the first element is an alphabetic + *KeySym* ``K`` for which both lowercase and uppercase forms are + defined. + + In that case, the group should be treated as if the first element were + the lowercase form of ``K`` and the second element were the uppercase + form of ``K``. + + This function assumes that *alphabetic* means *latin*; this assumption + appears to be consistent with observations of the return values from + ``XGetKeyboardMapping``. + + :param ks1: The first *keysym*. + + :param ks2: The second *keysym*. + + :return: a tuple conforming to the description above + """ + if ks2 == Xlib.XK.NoSymbol: + if keysym_is_latin_upper(ks1): + return (Xlib.XK.XK_a + ks1 - Xlib.XK.XK_A, ks1) + elif keysym_is_latin_lower(ks1): + return (ks1, Xlib.XK.XK_A + ks1 - Xlib.XK.XK_a) + else: + return (ks1, ks1) + else: + return (ks1, ks2) + + +def keysym_normalize(keysym): + """Normalises a list of *keysyms*. + + The implementation of this function comes from: + + If the list (ignoring trailing ``NoSymbol`` entries) is a single + *KeySym* ``K``, then the list is treated as if it were the list + ``K NoSymbol K NoSymbol``. + + If the list (ignoring trailing ``NoSymbol`` entries) is a pair of + *KeySyms* ``K1 K2``, then the list is treated as if it were the list + ``K1 K2 K1 K2``. + + If the list (ignoring trailing ``NoSymbol`` entries) is a triple of + *KeySyms* ``K1 K2 K3``, then the list is treated as if it were the list + ``K1 K2 K3 NoSymbol``. + + This function will also group the *keysyms* using :func:`keysym_group`. + + :param keysyms: A list of keysyms. + + :return: the tuple ``(group_1, group_2)`` or ``None`` + """ + # Remove trailing NoSymbol + stripped = list(reversed(list( + itertools.dropwhile( + lambda n: n == Xlib.XK.NoSymbol, + reversed(keysym))))) + + if not stripped: + return + + elif len(stripped) == 1: + return ( + keysym_group(stripped[0], Xlib.XK.NoSymbol), + keysym_group(stripped[0], Xlib.XK.NoSymbol)) + + elif len(stripped) == 2: + return ( + keysym_group(stripped[0], stripped[1]), + keysym_group(stripped[0], stripped[1])) + + elif len(stripped) == 3: + return ( + keysym_group(stripped[0], stripped[1]), + keysym_group(stripped[2], Xlib.XK.NoSymbol)) + + elif len(stripped) >= 6: + # TODO: Find out why this is necessary; using only the documented + # behaviour may lead to only a US layout being used? + return ( + keysym_group(stripped[0], stripped[1]), + keysym_group(stripped[4], stripped[5])) + + else: + return ( + keysym_group(stripped[0], stripped[1]), + keysym_group(stripped[2], stripped[3])) + + +def index_to_shift(display, index): + """Converts an index in a *key code* list to the corresponding shift state. + + :param Xlib.display.Display display: The display for which to retrieve the + shift mask. + + :param int index: The keyboard mapping *key code* index. + + :return: a shift mask + """ + return ( + (1 << 0 if index & 1 else 0) | + (alt_gr_mask(display) if index & 2 else 0)) + + +def shift_to_index(display, shift): + """Converts an index in a *key code* list to the corresponding shift state. + + :param Xlib.display.Display display: The display for which to retrieve the + shift mask. + + :param int index: The keyboard mapping *key code* index. + + :retur: a shift mask + """ + return ( + (1 if shift & 1 else 0) + + (2 if shift & alt_gr_mask(display) else 0)) + + +def keyboard_mapping(display): + """Generates a mapping from *keysyms* to *key codes* and required + modifier shift states. + + :param Xlib.display.Display display: The display for which to retrieve the + keyboard mapping. + + :return: the keyboard mapping + """ + mapping = {} + + shift_mask = 1 << 0 + group_mask = alt_gr_mask(display) + + # Iterate over all keysym lists in the keyboard mapping + min_keycode = display.display.info.min_keycode + keycode_count = display.display.info.max_keycode - min_keycode + 1 + for index, keysyms in enumerate(display.get_keyboard_mapping( + min_keycode, keycode_count)): + key_code = index + min_keycode + + # Normalise the keysym list to yield a tuple containing the two groups + normalized = keysym_normalize(keysyms) + if not normalized: + continue + + # Iterate over the groups to extract the shift and modifier state + for groups, group in zip(normalized, (False, True)): + for keysym, shift in zip(groups, (False, True)): + if not keysym: + continue + shift_state = 0 \ + | (shift_mask if shift else 0) \ + | (group_mask if group else 0) + + # Prefer already known lesser shift states + if keysym in mapping and mapping[keysym][1] < shift_state: + continue + mapping[keysym] = (key_code, shift_state) + + return mapping + + +def symbol_to_keysym(symbol): + """Converts a symbol name to a *keysym*. + + :param str symbol: The name of the symbol. + + :return: the corresponding *keysym*, or ``0`` if it cannot be found + """ + # First try simple translation + keysym = Xlib.XK.string_to_keysym(symbol) + if keysym: + return keysym + + # If that fails, try checking a module attribute of Xlib.keysymdef.xkb + if not keysym: + try: + return getattr(Xlib.keysymdef.xkb, 'XK_' + symbol, 0) + except AttributeError: + return SYMBOLS.get(symbol, (0,))[0] + + +class ListenerMixin(object): + """A mixin for *X* event listeners. + + Subclasses should set a value for :attr:`_EVENTS` and implement + :meth:`_handle`. + """ + #: The events for which to listen + _EVENTS = tuple() + + #: We use this instance for parsing the binary data + _EVENT_PARSER = Xlib.protocol.rq.EventField(None) + + def _run(self): + self._display_stop = Xlib.display.Display() + self._display_record = Xlib.display.Display() + with display_manager(self._display_stop) as dm: + self._context = dm.record_create_context( + 0, + [Xlib.ext.record.AllClients], + [{ + 'core_requests': (0, 0), + 'core_replies': (0, 0), + 'ext_requests': (0, 0, 0, 0), + 'ext_replies': (0, 0, 0, 0), + 'delivered_events': (0, 0), + 'device_events': self._EVENTS, + 'errors': (0, 0), + 'client_started': False, + 'client_died': False}]) + + # pylint: disable=W0702; we want to silence errors + try: + self._initialize(self._display_stop) + self._mark_ready() + if self.suppress: + with display_manager(self._display_record) as dm: + self._suppress_start(dm) + self._display_record.record_enable_context( + self._context, self._handler) + except: + # This exception will have been passed to the main thread + pass + finally: + if self.suppress: + with display_manager(self._display_stop) as dm: + self._suppress_stop(dm) + self._display_record.record_free_context(self._context) + self._display_stop.close() + self._display_record.close() + # pylint: enable=W0702 + + def _stop_platform(self): + if not hasattr(self, '_context'): + self.wait() + # pylint: disable=W0702; we must ignore errors + try: + with display_manager(self._display_stop) as dm: + dm.record_disable_context(self._context) + except: + pass + # pylint: enable=W0702 + + def _suppress_start(self, display): + """Starts suppressing events. + + :param Xlib.display.Display display: The display for which to suppress + events. + """ + raise NotImplementedError() + + def _suppress_stop(self, display): + """Starts suppressing events. + + :param Xlib.display.Display display: The display for which to suppress + events. + """ + raise NotImplementedError() + + @property + def _event_mask(self): + """The event mask. + """ + return functools.reduce(operator.__or__, self._EVENTS, 0) + + @AbstractListener._emitter + def _handler(self, events): + """The callback registered with *X* for mouse events. + + This method will parse the response and call the callbacks registered + on initialisation. + + :param events: The events passed by *X*. This is a binary block + parsable by :attr:`_EVENT_PARSER`. + """ + if not self.running: + raise self.StopException() + + data = events.data + + while data and len(data): + event, data = self._EVENT_PARSER.parse_binary_value( + data, self._display_record.display, None, None) + self._handle(self._display_stop, event) + + def _initialize(self, display): + """Initialises this listener. + + This method is called immediately before the event loop, from the + handler thread. + + :param display: The display being used. + """ + pass + + def _handle(self, display, event): + """The device specific callback handler. + + This method calls the appropriate callback registered when this + listener was created based on the event. + + :param display: The display being used. + + :param event: The event. + """ + pass diff --git a/pynput/_util/xorg_keysyms.py b/pynput/_util/xorg_keysyms.py new file mode 100644 index 0000000..c54fda6 --- /dev/null +++ b/pynput/_util/xorg_keysyms.py @@ -0,0 +1,1715 @@ +# coding: utf-8 +# pynput +# Copyright (C) 2015-2018 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +# pylint: disable=C0111,C0302 + +SYMBOLS = { + '0': (0x0030, u'\u0030'), + '1': (0x0031, u'\u0031'), + '2': (0x0032, u'\u0032'), + '3': (0x0033, u'\u0033'), + '4': (0x0034, u'\u0034'), + '5': (0x0035, u'\u0035'), + '6': (0x0036, u'\u0036'), + '7': (0x0037, u'\u0037'), + '8': (0x0038, u'\u0038'), + '9': (0x0039, u'\u0039'), + 'A': (0x0041, u'\u0041'), + 'AE': (0x00c6, u'\u00C6'), + 'Aacute': (0x00c1, u'\u00C1'), + 'Abelowdot': (0x1001ea0, u'\u1EA0'), + 'Abreve': (0x01c3, u'\u0102'), + 'Abreveacute': (0x1001eae, u'\u1EAE'), + 'Abrevebelowdot': (0x1001eb6, u'\u1EB6'), + 'Abrevegrave': (0x1001eb0, u'\u1EB0'), + 'Abrevehook': (0x1001eb2, u'\u1EB2'), + 'Abrevetilde': (0x1001eb4, u'\u1EB4'), + 'Acircumflex': (0x00c2, u'\u00C2'), + 'Acircumflexacute': (0x1001ea4, u'\u1EA4'), + 'Acircumflexbelowdot': (0x1001eac, u'\u1EAC'), + 'Acircumflexgrave': (0x1001ea6, u'\u1EA6'), + 'Acircumflexhook': (0x1001ea8, u'\u1EA8'), + 'Acircumflextilde': (0x1001eaa, u'\u1EAA'), + 'Adiaeresis': (0x00c4, u'\u00C4'), + 'Agrave': (0x00c0, u'\u00C0'), + 'Ahook': (0x1001ea2, u'\u1EA2'), + 'Amacron': (0x03c0, u'\u0100'), + 'Aogonek': (0x01a1, u'\u0104'), + 'Arabic_0': (0x1000660, u'\u0660'), + 'Arabic_1': (0x1000661, u'\u0661'), + 'Arabic_2': (0x1000662, u'\u0662'), + 'Arabic_3': (0x1000663, u'\u0663'), + 'Arabic_4': (0x1000664, u'\u0664'), + 'Arabic_5': (0x1000665, u'\u0665'), + 'Arabic_6': (0x1000666, u'\u0666'), + 'Arabic_7': (0x1000667, u'\u0667'), + 'Arabic_8': (0x1000668, u'\u0668'), + 'Arabic_9': (0x1000669, u'\u0669'), + 'Arabic_ain': (0x05d9, u'\u0639'), + 'Arabic_alef': (0x05c7, u'\u0627'), + 'Arabic_alefmaksura': (0x05e9, u'\u0649'), + 'Arabic_beh': (0x05c8, u'\u0628'), + 'Arabic_comma': (0x05ac, u'\u060C'), + 'Arabic_dad': (0x05d6, u'\u0636'), + 'Arabic_dal': (0x05cf, u'\u062F'), + 'Arabic_damma': (0x05ef, u'\u064F'), + 'Arabic_dammatan': (0x05ec, u'\u064C'), + 'Arabic_ddal': (0x1000688, u'\u0688'), + 'Arabic_farsi_yeh': (0x10006cc, u'\u06CC'), + 'Arabic_fatha': (0x05ee, u'\u064E'), + 'Arabic_fathatan': (0x05eb, u'\u064B'), + 'Arabic_feh': (0x05e1, u'\u0641'), + 'Arabic_fullstop': (0x10006d4, u'\u06D4'), + 'Arabic_gaf': (0x10006af, u'\u06AF'), + 'Arabic_ghain': (0x05da, u'\u063A'), + 'Arabic_ha': (0x05e7, u'\u0647'), + 'Arabic_hah': (0x05cd, u'\u062D'), + 'Arabic_hamza': (0x05c1, u'\u0621'), + 'Arabic_hamza_above': (0x1000654, u'\u0654'), + 'Arabic_hamza_below': (0x1000655, u'\u0655'), + 'Arabic_hamzaonalef': (0x05c3, u'\u0623'), + 'Arabic_hamzaonwaw': (0x05c4, u'\u0624'), + 'Arabic_hamzaonyeh': (0x05c6, u'\u0626'), + 'Arabic_hamzaunderalef': (0x05c5, u'\u0625'), + 'Arabic_heh_doachashmee': (0x10006be, u'\u06BE'), + 'Arabic_heh_goal': (0x10006c1, u'\u06C1'), + 'Arabic_jeem': (0x05cc, u'\u062C'), + 'Arabic_jeh': (0x1000698, u'\u0698'), + 'Arabic_kaf': (0x05e3, u'\u0643'), + 'Arabic_kasra': (0x05f0, u'\u0650'), + 'Arabic_kasratan': (0x05ed, u'\u064D'), + 'Arabic_keheh': (0x10006a9, u'\u06A9'), + 'Arabic_khah': (0x05ce, u'\u062E'), + 'Arabic_lam': (0x05e4, u'\u0644'), + 'Arabic_madda_above': (0x1000653, u'\u0653'), + 'Arabic_maddaonalef': (0x05c2, u'\u0622'), + 'Arabic_meem': (0x05e5, u'\u0645'), + 'Arabic_noon': (0x05e6, u'\u0646'), + 'Arabic_noon_ghunna': (0x10006ba, u'\u06BA'), + 'Arabic_peh': (0x100067e, u'\u067E'), + 'Arabic_percent': (0x100066a, u'\u066A'), + 'Arabic_qaf': (0x05e2, u'\u0642'), + 'Arabic_question_mark': (0x05bf, u'\u061F'), + 'Arabic_ra': (0x05d1, u'\u0631'), + 'Arabic_rreh': (0x1000691, u'\u0691'), + 'Arabic_sad': (0x05d5, u'\u0635'), + 'Arabic_seen': (0x05d3, u'\u0633'), + 'Arabic_semicolon': (0x05bb, u'\u061B'), + 'Arabic_shadda': (0x05f1, u'\u0651'), + 'Arabic_sheen': (0x05d4, u'\u0634'), + 'Arabic_sukun': (0x05f2, u'\u0652'), + 'Arabic_superscript_alef': (0x1000670, u'\u0670'), + 'Arabic_tah': (0x05d7, u'\u0637'), + 'Arabic_tatweel': (0x05e0, u'\u0640'), + 'Arabic_tcheh': (0x1000686, u'\u0686'), + 'Arabic_teh': (0x05ca, u'\u062A'), + 'Arabic_tehmarbuta': (0x05c9, u'\u0629'), + 'Arabic_thal': (0x05d0, u'\u0630'), + 'Arabic_theh': (0x05cb, u'\u062B'), + 'Arabic_tteh': (0x1000679, u'\u0679'), + 'Arabic_veh': (0x10006a4, u'\u06A4'), + 'Arabic_waw': (0x05e8, u'\u0648'), + 'Arabic_yeh': (0x05ea, u'\u064A'), + 'Arabic_yeh_baree': (0x10006d2, u'\u06D2'), + 'Arabic_zah': (0x05d8, u'\u0638'), + 'Arabic_zain': (0x05d2, u'\u0632'), + 'Aring': (0x00c5, u'\u00C5'), + 'Armenian_AT': (0x1000538, u'\u0538'), + 'Armenian_AYB': (0x1000531, u'\u0531'), + 'Armenian_BEN': (0x1000532, u'\u0532'), + 'Armenian_CHA': (0x1000549, u'\u0549'), + 'Armenian_DA': (0x1000534, u'\u0534'), + 'Armenian_DZA': (0x1000541, u'\u0541'), + 'Armenian_E': (0x1000537, u'\u0537'), + 'Armenian_FE': (0x1000556, u'\u0556'), + 'Armenian_GHAT': (0x1000542, u'\u0542'), + 'Armenian_GIM': (0x1000533, u'\u0533'), + 'Armenian_HI': (0x1000545, u'\u0545'), + 'Armenian_HO': (0x1000540, u'\u0540'), + 'Armenian_INI': (0x100053b, u'\u053B'), + 'Armenian_JE': (0x100054b, u'\u054B'), + 'Armenian_KE': (0x1000554, u'\u0554'), + 'Armenian_KEN': (0x100053f, u'\u053F'), + 'Armenian_KHE': (0x100053d, u'\u053D'), + 'Armenian_LYUN': (0x100053c, u'\u053C'), + 'Armenian_MEN': (0x1000544, u'\u0544'), + 'Armenian_NU': (0x1000546, u'\u0546'), + 'Armenian_O': (0x1000555, u'\u0555'), + 'Armenian_PE': (0x100054a, u'\u054A'), + 'Armenian_PYUR': (0x1000553, u'\u0553'), + 'Armenian_RA': (0x100054c, u'\u054C'), + 'Armenian_RE': (0x1000550, u'\u0550'), + 'Armenian_SE': (0x100054d, u'\u054D'), + 'Armenian_SHA': (0x1000547, u'\u0547'), + 'Armenian_TCHE': (0x1000543, u'\u0543'), + 'Armenian_TO': (0x1000539, u'\u0539'), + 'Armenian_TSA': (0x100053e, u'\u053E'), + 'Armenian_TSO': (0x1000551, u'\u0551'), + 'Armenian_TYUN': (0x100054f, u'\u054F'), + 'Armenian_VEV': (0x100054e, u'\u054E'), + 'Armenian_VO': (0x1000548, u'\u0548'), + 'Armenian_VYUN': (0x1000552, u'\u0552'), + 'Armenian_YECH': (0x1000535, u'\u0535'), + 'Armenian_ZA': (0x1000536, u'\u0536'), + 'Armenian_ZHE': (0x100053a, u'\u053A'), + 'Armenian_accent': (0x100055b, u'\u055B'), + 'Armenian_amanak': (0x100055c, u'\u055C'), + 'Armenian_apostrophe': (0x100055a, u'\u055A'), + 'Armenian_at': (0x1000568, u'\u0568'), + 'Armenian_ayb': (0x1000561, u'\u0561'), + 'Armenian_ben': (0x1000562, u'\u0562'), + 'Armenian_but': (0x100055d, u'\u055D'), + 'Armenian_cha': (0x1000579, u'\u0579'), + 'Armenian_da': (0x1000564, u'\u0564'), + 'Armenian_dza': (0x1000571, u'\u0571'), + 'Armenian_e': (0x1000567, u'\u0567'), + 'Armenian_exclam': (0x100055c, u'\u055C'), + 'Armenian_fe': (0x1000586, u'\u0586'), + 'Armenian_full_stop': (0x1000589, u'\u0589'), + 'Armenian_ghat': (0x1000572, u'\u0572'), + 'Armenian_gim': (0x1000563, u'\u0563'), + 'Armenian_hi': (0x1000575, u'\u0575'), + 'Armenian_ho': (0x1000570, u'\u0570'), + 'Armenian_hyphen': (0x100058a, u'\u058A'), + 'Armenian_ini': (0x100056b, u'\u056B'), + 'Armenian_je': (0x100057b, u'\u057B'), + 'Armenian_ke': (0x1000584, u'\u0584'), + 'Armenian_ken': (0x100056f, u'\u056F'), + 'Armenian_khe': (0x100056d, u'\u056D'), + 'Armenian_ligature_ew': (0x1000587, u'\u0587'), + 'Armenian_lyun': (0x100056c, u'\u056C'), + 'Armenian_men': (0x1000574, u'\u0574'), + 'Armenian_nu': (0x1000576, u'\u0576'), + 'Armenian_o': (0x1000585, u'\u0585'), + 'Armenian_paruyk': (0x100055e, u'\u055E'), + 'Armenian_pe': (0x100057a, u'\u057A'), + 'Armenian_pyur': (0x1000583, u'\u0583'), + 'Armenian_question': (0x100055e, u'\u055E'), + 'Armenian_ra': (0x100057c, u'\u057C'), + 'Armenian_re': (0x1000580, u'\u0580'), + 'Armenian_se': (0x100057d, u'\u057D'), + 'Armenian_separation_mark': (0x100055d, u'\u055D'), + 'Armenian_sha': (0x1000577, u'\u0577'), + 'Armenian_shesht': (0x100055b, u'\u055B'), + 'Armenian_tche': (0x1000573, u'\u0573'), + 'Armenian_to': (0x1000569, u'\u0569'), + 'Armenian_tsa': (0x100056e, u'\u056E'), + 'Armenian_tso': (0x1000581, u'\u0581'), + 'Armenian_tyun': (0x100057f, u'\u057F'), + 'Armenian_verjaket': (0x1000589, u'\u0589'), + 'Armenian_vev': (0x100057e, u'\u057E'), + 'Armenian_vo': (0x1000578, u'\u0578'), + 'Armenian_vyun': (0x1000582, u'\u0582'), + 'Armenian_yech': (0x1000565, u'\u0565'), + 'Armenian_yentamna': (0x100058a, u'\u058A'), + 'Armenian_za': (0x1000566, u'\u0566'), + 'Armenian_zhe': (0x100056a, u'\u056A'), + 'Atilde': (0x00c3, u'\u00C3'), + 'B': (0x0042, u'\u0042'), + 'Babovedot': (0x1001e02, u'\u1E02'), + 'Byelorussian_SHORTU': (0x06be, u'\u040E'), + 'Byelorussian_shortu': (0x06ae, u'\u045E'), + 'C': (0x0043, u'\u0043'), + 'Cabovedot': (0x02c5, u'\u010A'), + 'Cacute': (0x01c6, u'\u0106'), + 'Ccaron': (0x01c8, u'\u010C'), + 'Ccedilla': (0x00c7, u'\u00C7'), + 'Ccircumflex': (0x02c6, u'\u0108'), + 'ColonSign': (0x10020a1, u'\u20A1'), + 'CruzeiroSign': (0x10020a2, u'\u20A2'), + 'Cyrillic_A': (0x06e1, u'\u0410'), + 'Cyrillic_BE': (0x06e2, u'\u0411'), + 'Cyrillic_CHE': (0x06fe, u'\u0427'), + 'Cyrillic_CHE_descender': (0x10004b6, u'\u04B6'), + 'Cyrillic_CHE_vertstroke': (0x10004b8, u'\u04B8'), + 'Cyrillic_DE': (0x06e4, u'\u0414'), + 'Cyrillic_DZHE': (0x06bf, u'\u040F'), + 'Cyrillic_E': (0x06fc, u'\u042D'), + 'Cyrillic_EF': (0x06e6, u'\u0424'), + 'Cyrillic_EL': (0x06ec, u'\u041B'), + 'Cyrillic_EM': (0x06ed, u'\u041C'), + 'Cyrillic_EN': (0x06ee, u'\u041D'), + 'Cyrillic_EN_descender': (0x10004a2, u'\u04A2'), + 'Cyrillic_ER': (0x06f2, u'\u0420'), + 'Cyrillic_ES': (0x06f3, u'\u0421'), + 'Cyrillic_GHE': (0x06e7, u'\u0413'), + 'Cyrillic_GHE_bar': (0x1000492, u'\u0492'), + 'Cyrillic_HA': (0x06e8, u'\u0425'), + 'Cyrillic_HARDSIGN': (0x06ff, u'\u042A'), + 'Cyrillic_HA_descender': (0x10004b2, u'\u04B2'), + 'Cyrillic_I': (0x06e9, u'\u0418'), + 'Cyrillic_IE': (0x06e5, u'\u0415'), + 'Cyrillic_IO': (0x06b3, u'\u0401'), + 'Cyrillic_I_macron': (0x10004e2, u'\u04E2'), + 'Cyrillic_JE': (0x06b8, u'\u0408'), + 'Cyrillic_KA': (0x06eb, u'\u041A'), + 'Cyrillic_KA_descender': (0x100049a, u'\u049A'), + 'Cyrillic_KA_vertstroke': (0x100049c, u'\u049C'), + 'Cyrillic_LJE': (0x06b9, u'\u0409'), + 'Cyrillic_NJE': (0x06ba, u'\u040A'), + 'Cyrillic_O': (0x06ef, u'\u041E'), + 'Cyrillic_O_bar': (0x10004e8, u'\u04E8'), + 'Cyrillic_PE': (0x06f0, u'\u041F'), + 'Cyrillic_SCHWA': (0x10004d8, u'\u04D8'), + 'Cyrillic_SHA': (0x06fb, u'\u0428'), + 'Cyrillic_SHCHA': (0x06fd, u'\u0429'), + 'Cyrillic_SHHA': (0x10004ba, u'\u04BA'), + 'Cyrillic_SHORTI': (0x06ea, u'\u0419'), + 'Cyrillic_SOFTSIGN': (0x06f8, u'\u042C'), + 'Cyrillic_TE': (0x06f4, u'\u0422'), + 'Cyrillic_TSE': (0x06e3, u'\u0426'), + 'Cyrillic_U': (0x06f5, u'\u0423'), + 'Cyrillic_U_macron': (0x10004ee, u'\u04EE'), + 'Cyrillic_U_straight': (0x10004ae, u'\u04AE'), + 'Cyrillic_U_straight_bar': (0x10004b0, u'\u04B0'), + 'Cyrillic_VE': (0x06f7, u'\u0412'), + 'Cyrillic_YA': (0x06f1, u'\u042F'), + 'Cyrillic_YERU': (0x06f9, u'\u042B'), + 'Cyrillic_YU': (0x06e0, u'\u042E'), + 'Cyrillic_ZE': (0x06fa, u'\u0417'), + 'Cyrillic_ZHE': (0x06f6, u'\u0416'), + 'Cyrillic_ZHE_descender': (0x1000496, u'\u0496'), + 'Cyrillic_a': (0x06c1, u'\u0430'), + 'Cyrillic_be': (0x06c2, u'\u0431'), + 'Cyrillic_che': (0x06de, u'\u0447'), + 'Cyrillic_che_descender': (0x10004b7, u'\u04B7'), + 'Cyrillic_che_vertstroke': (0x10004b9, u'\u04B9'), + 'Cyrillic_de': (0x06c4, u'\u0434'), + 'Cyrillic_dzhe': (0x06af, u'\u045F'), + 'Cyrillic_e': (0x06dc, u'\u044D'), + 'Cyrillic_ef': (0x06c6, u'\u0444'), + 'Cyrillic_el': (0x06cc, u'\u043B'), + 'Cyrillic_em': (0x06cd, u'\u043C'), + 'Cyrillic_en': (0x06ce, u'\u043D'), + 'Cyrillic_en_descender': (0x10004a3, u'\u04A3'), + 'Cyrillic_er': (0x06d2, u'\u0440'), + 'Cyrillic_es': (0x06d3, u'\u0441'), + 'Cyrillic_ghe': (0x06c7, u'\u0433'), + 'Cyrillic_ghe_bar': (0x1000493, u'\u0493'), + 'Cyrillic_ha': (0x06c8, u'\u0445'), + 'Cyrillic_ha_descender': (0x10004b3, u'\u04B3'), + 'Cyrillic_hardsign': (0x06df, u'\u044A'), + 'Cyrillic_i': (0x06c9, u'\u0438'), + 'Cyrillic_i_macron': (0x10004e3, u'\u04E3'), + 'Cyrillic_ie': (0x06c5, u'\u0435'), + 'Cyrillic_io': (0x06a3, u'\u0451'), + 'Cyrillic_je': (0x06a8, u'\u0458'), + 'Cyrillic_ka': (0x06cb, u'\u043A'), + 'Cyrillic_ka_descender': (0x100049b, u'\u049B'), + 'Cyrillic_ka_vertstroke': (0x100049d, u'\u049D'), + 'Cyrillic_lje': (0x06a9, u'\u0459'), + 'Cyrillic_nje': (0x06aa, u'\u045A'), + 'Cyrillic_o': (0x06cf, u'\u043E'), + 'Cyrillic_o_bar': (0x10004e9, u'\u04E9'), + 'Cyrillic_pe': (0x06d0, u'\u043F'), + 'Cyrillic_schwa': (0x10004d9, u'\u04D9'), + 'Cyrillic_sha': (0x06db, u'\u0448'), + 'Cyrillic_shcha': (0x06dd, u'\u0449'), + 'Cyrillic_shha': (0x10004bb, u'\u04BB'), + 'Cyrillic_shorti': (0x06ca, u'\u0439'), + 'Cyrillic_softsign': (0x06d8, u'\u044C'), + 'Cyrillic_te': (0x06d4, u'\u0442'), + 'Cyrillic_tse': (0x06c3, u'\u0446'), + 'Cyrillic_u': (0x06d5, u'\u0443'), + 'Cyrillic_u_macron': (0x10004ef, u'\u04EF'), + 'Cyrillic_u_straight': (0x10004af, u'\u04AF'), + 'Cyrillic_u_straight_bar': (0x10004b1, u'\u04B1'), + 'Cyrillic_ve': (0x06d7, u'\u0432'), + 'Cyrillic_ya': (0x06d1, u'\u044F'), + 'Cyrillic_yeru': (0x06d9, u'\u044B'), + 'Cyrillic_yu': (0x06c0, u'\u044E'), + 'Cyrillic_ze': (0x06da, u'\u0437'), + 'Cyrillic_zhe': (0x06d6, u'\u0436'), + 'Cyrillic_zhe_descender': (0x1000497, u'\u0497'), + 'D': (0x0044, u'\u0044'), + 'Dabovedot': (0x1001e0a, u'\u1E0A'), + 'Dcaron': (0x01cf, u'\u010E'), + 'DongSign': (0x10020ab, u'\u20AB'), + 'Dstroke': (0x01d0, u'\u0110'), + 'E': (0x0045, u'\u0045'), + 'ENG': (0x03bd, u'\u014A'), + 'ETH': (0x00d0, u'\u00D0'), + 'EZH': (0x10001b7, u'\u01B7'), + 'Eabovedot': (0x03cc, u'\u0116'), + 'Eacute': (0x00c9, u'\u00C9'), + 'Ebelowdot': (0x1001eb8, u'\u1EB8'), + 'Ecaron': (0x01cc, u'\u011A'), + 'Ecircumflex': (0x00ca, u'\u00CA'), + 'Ecircumflexacute': (0x1001ebe, u'\u1EBE'), + 'Ecircumflexbelowdot': (0x1001ec6, u'\u1EC6'), + 'Ecircumflexgrave': (0x1001ec0, u'\u1EC0'), + 'Ecircumflexhook': (0x1001ec2, u'\u1EC2'), + 'Ecircumflextilde': (0x1001ec4, u'\u1EC4'), + 'EcuSign': (0x10020a0, u'\u20A0'), + 'Ediaeresis': (0x00cb, u'\u00CB'), + 'Egrave': (0x00c8, u'\u00C8'), + 'Ehook': (0x1001eba, u'\u1EBA'), + 'Emacron': (0x03aa, u'\u0112'), + 'Eogonek': (0x01ca, u'\u0118'), + 'Etilde': (0x1001ebc, u'\u1EBC'), + 'EuroSign': (0x20ac, u'\u20AC'), + 'F': (0x0046, u'\u0046'), + 'FFrancSign': (0x10020a3, u'\u20A3'), + 'Fabovedot': (0x1001e1e, u'\u1E1E'), + 'Farsi_0': (0x10006f0, u'\u06F0'), + 'Farsi_1': (0x10006f1, u'\u06F1'), + 'Farsi_2': (0x10006f2, u'\u06F2'), + 'Farsi_3': (0x10006f3, u'\u06F3'), + 'Farsi_4': (0x10006f4, u'\u06F4'), + 'Farsi_5': (0x10006f5, u'\u06F5'), + 'Farsi_6': (0x10006f6, u'\u06F6'), + 'Farsi_7': (0x10006f7, u'\u06F7'), + 'Farsi_8': (0x10006f8, u'\u06F8'), + 'Farsi_9': (0x10006f9, u'\u06F9'), + 'Farsi_yeh': (0x10006cc, u'\u06CC'), + 'G': (0x0047, u'\u0047'), + 'Gabovedot': (0x02d5, u'\u0120'), + 'Gbreve': (0x02ab, u'\u011E'), + 'Gcaron': (0x10001e6, u'\u01E6'), + 'Gcedilla': (0x03ab, u'\u0122'), + 'Gcircumflex': (0x02d8, u'\u011C'), + 'Georgian_an': (0x10010d0, u'\u10D0'), + 'Georgian_ban': (0x10010d1, u'\u10D1'), + 'Georgian_can': (0x10010ea, u'\u10EA'), + 'Georgian_char': (0x10010ed, u'\u10ED'), + 'Georgian_chin': (0x10010e9, u'\u10E9'), + 'Georgian_cil': (0x10010ec, u'\u10EC'), + 'Georgian_don': (0x10010d3, u'\u10D3'), + 'Georgian_en': (0x10010d4, u'\u10D4'), + 'Georgian_fi': (0x10010f6, u'\u10F6'), + 'Georgian_gan': (0x10010d2, u'\u10D2'), + 'Georgian_ghan': (0x10010e6, u'\u10E6'), + 'Georgian_hae': (0x10010f0, u'\u10F0'), + 'Georgian_har': (0x10010f4, u'\u10F4'), + 'Georgian_he': (0x10010f1, u'\u10F1'), + 'Georgian_hie': (0x10010f2, u'\u10F2'), + 'Georgian_hoe': (0x10010f5, u'\u10F5'), + 'Georgian_in': (0x10010d8, u'\u10D8'), + 'Georgian_jhan': (0x10010ef, u'\u10EF'), + 'Georgian_jil': (0x10010eb, u'\u10EB'), + 'Georgian_kan': (0x10010d9, u'\u10D9'), + 'Georgian_khar': (0x10010e5, u'\u10E5'), + 'Georgian_las': (0x10010da, u'\u10DA'), + 'Georgian_man': (0x10010db, u'\u10DB'), + 'Georgian_nar': (0x10010dc, u'\u10DC'), + 'Georgian_on': (0x10010dd, u'\u10DD'), + 'Georgian_par': (0x10010de, u'\u10DE'), + 'Georgian_phar': (0x10010e4, u'\u10E4'), + 'Georgian_qar': (0x10010e7, u'\u10E7'), + 'Georgian_rae': (0x10010e0, u'\u10E0'), + 'Georgian_san': (0x10010e1, u'\u10E1'), + 'Georgian_shin': (0x10010e8, u'\u10E8'), + 'Georgian_tan': (0x10010d7, u'\u10D7'), + 'Georgian_tar': (0x10010e2, u'\u10E2'), + 'Georgian_un': (0x10010e3, u'\u10E3'), + 'Georgian_vin': (0x10010d5, u'\u10D5'), + 'Georgian_we': (0x10010f3, u'\u10F3'), + 'Georgian_xan': (0x10010ee, u'\u10EE'), + 'Georgian_zen': (0x10010d6, u'\u10D6'), + 'Georgian_zhar': (0x10010df, u'\u10DF'), + 'Greek_ALPHA': (0x07c1, u'\u0391'), + 'Greek_ALPHAaccent': (0x07a1, u'\u0386'), + 'Greek_BETA': (0x07c2, u'\u0392'), + 'Greek_CHI': (0x07d7, u'\u03A7'), + 'Greek_DELTA': (0x07c4, u'\u0394'), + 'Greek_EPSILON': (0x07c5, u'\u0395'), + 'Greek_EPSILONaccent': (0x07a2, u'\u0388'), + 'Greek_ETA': (0x07c7, u'\u0397'), + 'Greek_ETAaccent': (0x07a3, u'\u0389'), + 'Greek_GAMMA': (0x07c3, u'\u0393'), + 'Greek_IOTA': (0x07c9, u'\u0399'), + 'Greek_IOTAaccent': (0x07a4, u'\u038A'), + 'Greek_IOTAdieresis': (0x07a5, u'\u03AA'), + 'Greek_KAPPA': (0x07ca, u'\u039A'), + 'Greek_LAMBDA': (0x07cb, u'\u039B'), + 'Greek_LAMDA': (0x07cb, u'\u039B'), + 'Greek_MU': (0x07cc, u'\u039C'), + 'Greek_NU': (0x07cd, u'\u039D'), + 'Greek_OMEGA': (0x07d9, u'\u03A9'), + 'Greek_OMEGAaccent': (0x07ab, u'\u038F'), + 'Greek_OMICRON': (0x07cf, u'\u039F'), + 'Greek_OMICRONaccent': (0x07a7, u'\u038C'), + 'Greek_PHI': (0x07d6, u'\u03A6'), + 'Greek_PI': (0x07d0, u'\u03A0'), + 'Greek_PSI': (0x07d8, u'\u03A8'), + 'Greek_RHO': (0x07d1, u'\u03A1'), + 'Greek_SIGMA': (0x07d2, u'\u03A3'), + 'Greek_TAU': (0x07d4, u'\u03A4'), + 'Greek_THETA': (0x07c8, u'\u0398'), + 'Greek_UPSILON': (0x07d5, u'\u03A5'), + 'Greek_UPSILONaccent': (0x07a8, u'\u038E'), + 'Greek_UPSILONdieresis': (0x07a9, u'\u03AB'), + 'Greek_XI': (0x07ce, u'\u039E'), + 'Greek_ZETA': (0x07c6, u'\u0396'), + 'Greek_accentdieresis': (0x07ae, u'\u0385'), + 'Greek_alpha': (0x07e1, u'\u03B1'), + 'Greek_alphaaccent': (0x07b1, u'\u03AC'), + 'Greek_beta': (0x07e2, u'\u03B2'), + 'Greek_chi': (0x07f7, u'\u03C7'), + 'Greek_delta': (0x07e4, u'\u03B4'), + 'Greek_epsilon': (0x07e5, u'\u03B5'), + 'Greek_epsilonaccent': (0x07b2, u'\u03AD'), + 'Greek_eta': (0x07e7, u'\u03B7'), + 'Greek_etaaccent': (0x07b3, u'\u03AE'), + 'Greek_finalsmallsigma': (0x07f3, u'\u03C2'), + 'Greek_gamma': (0x07e3, u'\u03B3'), + 'Greek_horizbar': (0x07af, u'\u2015'), + 'Greek_iota': (0x07e9, u'\u03B9'), + 'Greek_iotaaccent': (0x07b4, u'\u03AF'), + 'Greek_iotaaccentdieresis': (0x07b6, u'\u0390'), + 'Greek_iotadieresis': (0x07b5, u'\u03CA'), + 'Greek_kappa': (0x07ea, u'\u03BA'), + 'Greek_lambda': (0x07eb, u'\u03BB'), + 'Greek_lamda': (0x07eb, u'\u03BB'), + 'Greek_mu': (0x07ec, u'\u03BC'), + 'Greek_nu': (0x07ed, u'\u03BD'), + 'Greek_omega': (0x07f9, u'\u03C9'), + 'Greek_omegaaccent': (0x07bb, u'\u03CE'), + 'Greek_omicron': (0x07ef, u'\u03BF'), + 'Greek_omicronaccent': (0x07b7, u'\u03CC'), + 'Greek_phi': (0x07f6, u'\u03C6'), + 'Greek_pi': (0x07f0, u'\u03C0'), + 'Greek_psi': (0x07f8, u'\u03C8'), + 'Greek_rho': (0x07f1, u'\u03C1'), + 'Greek_sigma': (0x07f2, u'\u03C3'), + 'Greek_tau': (0x07f4, u'\u03C4'), + 'Greek_theta': (0x07e8, u'\u03B8'), + 'Greek_upsilon': (0x07f5, u'\u03C5'), + 'Greek_upsilonaccent': (0x07b8, u'\u03CD'), + 'Greek_upsilonaccentdieresis': (0x07ba, u'\u03B0'), + 'Greek_upsilondieresis': (0x07b9, u'\u03CB'), + 'Greek_xi': (0x07ee, u'\u03BE'), + 'Greek_zeta': (0x07e6, u'\u03B6'), + 'H': (0x0048, u'\u0048'), + 'Hcircumflex': (0x02a6, u'\u0124'), + 'Hstroke': (0x02a1, u'\u0126'), + 'I': (0x0049, u'\u0049'), + 'Iabovedot': (0x02a9, u'\u0130'), + 'Iacute': (0x00cd, u'\u00CD'), + 'Ibelowdot': (0x1001eca, u'\u1ECA'), + 'Ibreve': (0x100012c, u'\u012C'), + 'Icircumflex': (0x00ce, u'\u00CE'), + 'Idiaeresis': (0x00cf, u'\u00CF'), + 'Igrave': (0x00cc, u'\u00CC'), + 'Ihook': (0x1001ec8, u'\u1EC8'), + 'Imacron': (0x03cf, u'\u012A'), + 'Iogonek': (0x03c7, u'\u012E'), + 'Itilde': (0x03a5, u'\u0128'), + 'J': (0x004a, u'\u004A'), + 'Jcircumflex': (0x02ac, u'\u0134'), + 'K': (0x004b, u'\u004B'), + 'KP_0': (0xffb0, None), + 'KP_1': (0xffb1, None), + 'KP_2': (0xffb2, None), + 'KP_3': (0xffb3, None), + 'KP_4': (0xffb4, None), + 'KP_5': (0xffb5, None), + 'KP_6': (0xffb6, None), + 'KP_7': (0xffb7, None), + 'KP_8': (0xffb8, None), + 'KP_9': (0xffb9, None), + 'KP_Add': (0xffab, None), + 'KP_Begin': (0xff9d, None), + 'KP_Decimal': (0xffae, None), + 'KP_Delete': (0xff9f, None), + 'KP_Divide': (0xffaf, None), + 'KP_Down': (0xff99, None), + 'KP_End': (0xff9c, None), + 'KP_Enter': (0xff8d, None), + 'KP_Equal': (0xffbd, None), + 'KP_F1': (0xff91, None), + 'KP_F2': (0xff92, None), + 'KP_F3': (0xff93, None), + 'KP_F4': (0xff94, None), + 'KP_Home': (0xff95, None), + 'KP_Insert': (0xff9e, None), + 'KP_Left': (0xff96, None), + 'KP_Multiply': (0xffaa, None), + 'KP_Next': (0xff9b, None), + 'KP_Page_Down': (0xff9b, None), + 'KP_Page_Up': (0xff9a, None), + 'KP_Prior': (0xff9a, None), + 'KP_Right': (0xff98, None), + 'KP_Separator': (0xffac, None), + 'KP_Space': (0xff80, None), + 'KP_Subtract': (0xffad, None), + 'KP_Tab': (0xff89, None), + 'KP_Up': (0xff97, None), + 'Kcedilla': (0x03d3, u'\u0136'), + 'L': (0x004c, u'\u004C'), + 'Lacute': (0x01c5, u'\u0139'), + 'Lbelowdot': (0x1001e36, u'\u1E36'), + 'Lcaron': (0x01a5, u'\u013D'), + 'Lcedilla': (0x03a6, u'\u013B'), + 'LiraSign': (0x10020a4, u'\u20A4'), + 'Lstroke': (0x01a3, u'\u0141'), + 'M': (0x004d, u'\u004D'), + 'Mabovedot': (0x1001e40, u'\u1E40'), + 'Macedonia_DSE': (0x06b5, u'\u0405'), + 'Macedonia_GJE': (0x06b2, u'\u0403'), + 'Macedonia_KJE': (0x06bc, u'\u040C'), + 'Macedonia_dse': (0x06a5, u'\u0455'), + 'Macedonia_gje': (0x06a2, u'\u0453'), + 'Macedonia_kje': (0x06ac, u'\u045C'), + 'MillSign': (0x10020a5, u'\u20A5'), + 'N': (0x004e, u'\u004E'), + 'Nacute': (0x01d1, u'\u0143'), + 'NairaSign': (0x10020a6, u'\u20A6'), + 'Ncaron': (0x01d2, u'\u0147'), + 'Ncedilla': (0x03d1, u'\u0145'), + 'NewSheqelSign': (0x10020aa, u'\u20AA'), + 'Ntilde': (0x00d1, u'\u00D1'), + 'O': (0x004f, u'\u004F'), + 'OE': (0x13bc, u'\u0152'), + 'Oacute': (0x00d3, u'\u00D3'), + 'Obarred': (0x100019f, u'\u019F'), + 'Obelowdot': (0x1001ecc, u'\u1ECC'), + 'Ocaron': (0x10001d1, u'\u01D2'), + 'Ocircumflex': (0x00d4, u'\u00D4'), + 'Ocircumflexacute': (0x1001ed0, u'\u1ED0'), + 'Ocircumflexbelowdot': (0x1001ed8, u'\u1ED8'), + 'Ocircumflexgrave': (0x1001ed2, u'\u1ED2'), + 'Ocircumflexhook': (0x1001ed4, u'\u1ED4'), + 'Ocircumflextilde': (0x1001ed6, u'\u1ED6'), + 'Odiaeresis': (0x00d6, u'\u00D6'), + 'Odoubleacute': (0x01d5, u'\u0150'), + 'Ograve': (0x00d2, u'\u00D2'), + 'Ohook': (0x1001ece, u'\u1ECE'), + 'Ohorn': (0x10001a0, u'\u01A0'), + 'Ohornacute': (0x1001eda, u'\u1EDA'), + 'Ohornbelowdot': (0x1001ee2, u'\u1EE2'), + 'Ohorngrave': (0x1001edc, u'\u1EDC'), + 'Ohornhook': (0x1001ede, u'\u1EDE'), + 'Ohorntilde': (0x1001ee0, u'\u1EE0'), + 'Omacron': (0x03d2, u'\u014C'), + 'Ooblique': (0x00d8, u'\u00D8'), + 'Oslash': (0x00d8, u'\u00D8'), + 'Otilde': (0x00d5, u'\u00D5'), + 'P': (0x0050, u'\u0050'), + 'Pabovedot': (0x1001e56, u'\u1E56'), + 'PesetaSign': (0x10020a7, u'\u20A7'), + 'Q': (0x0051, u'\u0051'), + 'R': (0x0052, u'\u0052'), + 'Racute': (0x01c0, u'\u0154'), + 'Rcaron': (0x01d8, u'\u0158'), + 'Rcedilla': (0x03a3, u'\u0156'), + 'RupeeSign': (0x10020a8, u'\u20A8'), + 'S': (0x0053, u'\u0053'), + 'SCHWA': (0x100018f, u'\u018F'), + 'Sabovedot': (0x1001e60, u'\u1E60'), + 'Sacute': (0x01a6, u'\u015A'), + 'Scaron': (0x01a9, u'\u0160'), + 'Scedilla': (0x01aa, u'\u015E'), + 'Scircumflex': (0x02de, u'\u015C'), + 'Serbian_DJE': (0x06b1, u'\u0402'), + 'Serbian_TSHE': (0x06bb, u'\u040B'), + 'Serbian_dje': (0x06a1, u'\u0452'), + 'Serbian_tshe': (0x06ab, u'\u045B'), + 'Sinh_a': (0x1000d85, u'\u0D85'), + 'Sinh_aa': (0x1000d86, u'\u0D86'), + 'Sinh_aa2': (0x1000dcf, u'\u0DCF'), + 'Sinh_ae': (0x1000d87, u'\u0D87'), + 'Sinh_ae2': (0x1000dd0, u'\u0DD0'), + 'Sinh_aee': (0x1000d88, u'\u0D88'), + 'Sinh_aee2': (0x1000dd1, u'\u0DD1'), + 'Sinh_ai': (0x1000d93, u'\u0D93'), + 'Sinh_ai2': (0x1000ddb, u'\u0DDB'), + 'Sinh_al': (0x1000dca, u'\u0DCA'), + 'Sinh_au': (0x1000d96, u'\u0D96'), + 'Sinh_au2': (0x1000dde, u'\u0DDE'), + 'Sinh_ba': (0x1000db6, u'\u0DB6'), + 'Sinh_bha': (0x1000db7, u'\u0DB7'), + 'Sinh_ca': (0x1000da0, u'\u0DA0'), + 'Sinh_cha': (0x1000da1, u'\u0DA1'), + 'Sinh_dda': (0x1000da9, u'\u0DA9'), + 'Sinh_ddha': (0x1000daa, u'\u0DAA'), + 'Sinh_dha': (0x1000daf, u'\u0DAF'), + 'Sinh_dhha': (0x1000db0, u'\u0DB0'), + 'Sinh_e': (0x1000d91, u'\u0D91'), + 'Sinh_e2': (0x1000dd9, u'\u0DD9'), + 'Sinh_ee': (0x1000d92, u'\u0D92'), + 'Sinh_ee2': (0x1000dda, u'\u0DDA'), + 'Sinh_fa': (0x1000dc6, u'\u0DC6'), + 'Sinh_ga': (0x1000d9c, u'\u0D9C'), + 'Sinh_gha': (0x1000d9d, u'\u0D9D'), + 'Sinh_h2': (0x1000d83, u'\u0D83'), + 'Sinh_ha': (0x1000dc4, u'\u0DC4'), + 'Sinh_i': (0x1000d89, u'\u0D89'), + 'Sinh_i2': (0x1000dd2, u'\u0DD2'), + 'Sinh_ii': (0x1000d8a, u'\u0D8A'), + 'Sinh_ii2': (0x1000dd3, u'\u0DD3'), + 'Sinh_ja': (0x1000da2, u'\u0DA2'), + 'Sinh_jha': (0x1000da3, u'\u0DA3'), + 'Sinh_jnya': (0x1000da5, u'\u0DA5'), + 'Sinh_ka': (0x1000d9a, u'\u0D9A'), + 'Sinh_kha': (0x1000d9b, u'\u0D9B'), + 'Sinh_kunddaliya': (0x1000df4, u'\u0DF4'), + 'Sinh_la': (0x1000dbd, u'\u0DBD'), + 'Sinh_lla': (0x1000dc5, u'\u0DC5'), + 'Sinh_lu': (0x1000d8f, u'\u0D8F'), + 'Sinh_lu2': (0x1000ddf, u'\u0DDF'), + 'Sinh_luu': (0x1000d90, u'\u0D90'), + 'Sinh_luu2': (0x1000df3, u'\u0DF3'), + 'Sinh_ma': (0x1000db8, u'\u0DB8'), + 'Sinh_mba': (0x1000db9, u'\u0DB9'), + 'Sinh_na': (0x1000db1, u'\u0DB1'), + 'Sinh_ndda': (0x1000dac, u'\u0DAC'), + 'Sinh_ndha': (0x1000db3, u'\u0DB3'), + 'Sinh_ng': (0x1000d82, u'\u0D82'), + 'Sinh_ng2': (0x1000d9e, u'\u0D9E'), + 'Sinh_nga': (0x1000d9f, u'\u0D9F'), + 'Sinh_nja': (0x1000da6, u'\u0DA6'), + 'Sinh_nna': (0x1000dab, u'\u0DAB'), + 'Sinh_nya': (0x1000da4, u'\u0DA4'), + 'Sinh_o': (0x1000d94, u'\u0D94'), + 'Sinh_o2': (0x1000ddc, u'\u0DDC'), + 'Sinh_oo': (0x1000d95, u'\u0D95'), + 'Sinh_oo2': (0x1000ddd, u'\u0DDD'), + 'Sinh_pa': (0x1000db4, u'\u0DB4'), + 'Sinh_pha': (0x1000db5, u'\u0DB5'), + 'Sinh_ra': (0x1000dbb, u'\u0DBB'), + 'Sinh_ri': (0x1000d8d, u'\u0D8D'), + 'Sinh_rii': (0x1000d8e, u'\u0D8E'), + 'Sinh_ru2': (0x1000dd8, u'\u0DD8'), + 'Sinh_ruu2': (0x1000df2, u'\u0DF2'), + 'Sinh_sa': (0x1000dc3, u'\u0DC3'), + 'Sinh_sha': (0x1000dc1, u'\u0DC1'), + 'Sinh_ssha': (0x1000dc2, u'\u0DC2'), + 'Sinh_tha': (0x1000dad, u'\u0DAD'), + 'Sinh_thha': (0x1000dae, u'\u0DAE'), + 'Sinh_tta': (0x1000da7, u'\u0DA7'), + 'Sinh_ttha': (0x1000da8, u'\u0DA8'), + 'Sinh_u': (0x1000d8b, u'\u0D8B'), + 'Sinh_u2': (0x1000dd4, u'\u0DD4'), + 'Sinh_uu': (0x1000d8c, u'\u0D8C'), + 'Sinh_uu2': (0x1000dd6, u'\u0DD6'), + 'Sinh_va': (0x1000dc0, u'\u0DC0'), + 'Sinh_ya': (0x1000dba, u'\u0DBA'), + 'T': (0x0054, u'\u0054'), + 'THORN': (0x00de, u'\u00DE'), + 'Tabovedot': (0x1001e6a, u'\u1E6A'), + 'Tcaron': (0x01ab, u'\u0164'), + 'Tcedilla': (0x01de, u'\u0162'), + 'Thai_baht': (0x0ddf, u'\u0E3F'), + 'Thai_bobaimai': (0x0dba, u'\u0E1A'), + 'Thai_chochan': (0x0da8, u'\u0E08'), + 'Thai_chochang': (0x0daa, u'\u0E0A'), + 'Thai_choching': (0x0da9, u'\u0E09'), + 'Thai_chochoe': (0x0dac, u'\u0E0C'), + 'Thai_dochada': (0x0dae, u'\u0E0E'), + 'Thai_dodek': (0x0db4, u'\u0E14'), + 'Thai_fofa': (0x0dbd, u'\u0E1D'), + 'Thai_fofan': (0x0dbf, u'\u0E1F'), + 'Thai_hohip': (0x0dcb, u'\u0E2B'), + 'Thai_honokhuk': (0x0dce, u'\u0E2E'), + 'Thai_khokhai': (0x0da2, u'\u0E02'), + 'Thai_khokhon': (0x0da5, u'\u0E05'), + 'Thai_khokhuat': (0x0da3, u'\u0E03'), + 'Thai_khokhwai': (0x0da4, u'\u0E04'), + 'Thai_khorakhang': (0x0da6, u'\u0E06'), + 'Thai_kokai': (0x0da1, u'\u0E01'), + 'Thai_lakkhangyao': (0x0de5, u'\u0E45'), + 'Thai_lekchet': (0x0df7, u'\u0E57'), + 'Thai_lekha': (0x0df5, u'\u0E55'), + 'Thai_lekhok': (0x0df6, u'\u0E56'), + 'Thai_lekkao': (0x0df9, u'\u0E59'), + 'Thai_leknung': (0x0df1, u'\u0E51'), + 'Thai_lekpaet': (0x0df8, u'\u0E58'), + 'Thai_leksam': (0x0df3, u'\u0E53'), + 'Thai_leksi': (0x0df4, u'\u0E54'), + 'Thai_leksong': (0x0df2, u'\u0E52'), + 'Thai_leksun': (0x0df0, u'\u0E50'), + 'Thai_lochula': (0x0dcc, u'\u0E2C'), + 'Thai_loling': (0x0dc5, u'\u0E25'), + 'Thai_lu': (0x0dc6, u'\u0E26'), + 'Thai_maichattawa': (0x0deb, u'\u0E4B'), + 'Thai_maiek': (0x0de8, u'\u0E48'), + 'Thai_maihanakat': (0x0dd1, u'\u0E31'), + 'Thai_maitaikhu': (0x0de7, u'\u0E47'), + 'Thai_maitho': (0x0de9, u'\u0E49'), + 'Thai_maitri': (0x0dea, u'\u0E4A'), + 'Thai_maiyamok': (0x0de6, u'\u0E46'), + 'Thai_moma': (0x0dc1, u'\u0E21'), + 'Thai_ngongu': (0x0da7, u'\u0E07'), + 'Thai_nikhahit': (0x0ded, u'\u0E4D'), + 'Thai_nonen': (0x0db3, u'\u0E13'), + 'Thai_nonu': (0x0db9, u'\u0E19'), + 'Thai_oang': (0x0dcd, u'\u0E2D'), + 'Thai_paiyannoi': (0x0dcf, u'\u0E2F'), + 'Thai_phinthu': (0x0dda, u'\u0E3A'), + 'Thai_phophan': (0x0dbe, u'\u0E1E'), + 'Thai_phophung': (0x0dbc, u'\u0E1C'), + 'Thai_phosamphao': (0x0dc0, u'\u0E20'), + 'Thai_popla': (0x0dbb, u'\u0E1B'), + 'Thai_rorua': (0x0dc3, u'\u0E23'), + 'Thai_ru': (0x0dc4, u'\u0E24'), + 'Thai_saraa': (0x0dd0, u'\u0E30'), + 'Thai_saraaa': (0x0dd2, u'\u0E32'), + 'Thai_saraae': (0x0de1, u'\u0E41'), + 'Thai_saraaimaimalai': (0x0de4, u'\u0E44'), + 'Thai_saraaimaimuan': (0x0de3, u'\u0E43'), + 'Thai_saraam': (0x0dd3, u'\u0E33'), + 'Thai_sarae': (0x0de0, u'\u0E40'), + 'Thai_sarai': (0x0dd4, u'\u0E34'), + 'Thai_saraii': (0x0dd5, u'\u0E35'), + 'Thai_sarao': (0x0de2, u'\u0E42'), + 'Thai_sarau': (0x0dd8, u'\u0E38'), + 'Thai_saraue': (0x0dd6, u'\u0E36'), + 'Thai_sarauee': (0x0dd7, u'\u0E37'), + 'Thai_sarauu': (0x0dd9, u'\u0E39'), + 'Thai_sorusi': (0x0dc9, u'\u0E29'), + 'Thai_sosala': (0x0dc8, u'\u0E28'), + 'Thai_soso': (0x0dab, u'\u0E0B'), + 'Thai_sosua': (0x0dca, u'\u0E2A'), + 'Thai_thanthakhat': (0x0dec, u'\u0E4C'), + 'Thai_thonangmontho': (0x0db1, u'\u0E11'), + 'Thai_thophuthao': (0x0db2, u'\u0E12'), + 'Thai_thothahan': (0x0db7, u'\u0E17'), + 'Thai_thothan': (0x0db0, u'\u0E10'), + 'Thai_thothong': (0x0db8, u'\u0E18'), + 'Thai_thothung': (0x0db6, u'\u0E16'), + 'Thai_topatak': (0x0daf, u'\u0E0F'), + 'Thai_totao': (0x0db5, u'\u0E15'), + 'Thai_wowaen': (0x0dc7, u'\u0E27'), + 'Thai_yoyak': (0x0dc2, u'\u0E22'), + 'Thai_yoying': (0x0dad, u'\u0E0D'), + 'Tslash': (0x03ac, u'\u0166'), + 'U': (0x0055, u'\u0055'), + 'Uacute': (0x00da, u'\u00DA'), + 'Ubelowdot': (0x1001ee4, u'\u1EE4'), + 'Ubreve': (0x02dd, u'\u016C'), + 'Ucircumflex': (0x00db, u'\u00DB'), + 'Udiaeresis': (0x00dc, u'\u00DC'), + 'Udoubleacute': (0x01db, u'\u0170'), + 'Ugrave': (0x00d9, u'\u00D9'), + 'Uhook': (0x1001ee6, u'\u1EE6'), + 'Uhorn': (0x10001af, u'\u01AF'), + 'Uhornacute': (0x1001ee8, u'\u1EE8'), + 'Uhornbelowdot': (0x1001ef0, u'\u1EF0'), + 'Uhorngrave': (0x1001eea, u'\u1EEA'), + 'Uhornhook': (0x1001eec, u'\u1EEC'), + 'Uhorntilde': (0x1001eee, u'\u1EEE'), + 'Ukrainian_GHE_WITH_UPTURN': (0x06bd, u'\u0490'), + 'Ukrainian_I': (0x06b6, u'\u0406'), + 'Ukrainian_IE': (0x06b4, u'\u0404'), + 'Ukrainian_YI': (0x06b7, u'\u0407'), + 'Ukrainian_ghe_with_upturn': (0x06ad, u'\u0491'), + 'Ukrainian_i': (0x06a6, u'\u0456'), + 'Ukrainian_ie': (0x06a4, u'\u0454'), + 'Ukrainian_yi': (0x06a7, u'\u0457'), + 'Umacron': (0x03de, u'\u016A'), + 'Uogonek': (0x03d9, u'\u0172'), + 'Uring': (0x01d9, u'\u016E'), + 'Utilde': (0x03dd, u'\u0168'), + 'V': (0x0056, u'\u0056'), + 'W': (0x0057, u'\u0057'), + 'Wacute': (0x1001e82, u'\u1E82'), + 'Wcircumflex': (0x1000174, u'\u0174'), + 'Wdiaeresis': (0x1001e84, u'\u1E84'), + 'Wgrave': (0x1001e80, u'\u1E80'), + 'WonSign': (0x10020a9, u'\u20A9'), + 'X': (0x0058, u'\u0058'), + 'Xabovedot': (0x1001e8a, u'\u1E8A'), + 'Y': (0x0059, u'\u0059'), + 'Yacute': (0x00dd, u'\u00DD'), + 'Ybelowdot': (0x1001ef4, u'\u1EF4'), + 'Ycircumflex': (0x1000176, u'\u0176'), + 'Ydiaeresis': (0x13be, u'\u0178'), + 'Ygrave': (0x1001ef2, u'\u1EF2'), + 'Yhook': (0x1001ef6, u'\u1EF6'), + 'Ytilde': (0x1001ef8, u'\u1EF8'), + 'Z': (0x005a, u'\u005A'), + 'Zabovedot': (0x01af, u'\u017B'), + 'Zacute': (0x01ac, u'\u0179'), + 'Zcaron': (0x01ae, u'\u017D'), + 'Zstroke': (0x10001b5, u'\u01B5'), + 'a': (0x0061, u'\u0061'), + 'aacute': (0x00e1, u'\u00E1'), + 'abelowdot': (0x1001ea1, u'\u1EA1'), + 'abovedot': (0x01ff, u'\u02D9'), + 'abreve': (0x01e3, u'\u0103'), + 'abreveacute': (0x1001eaf, u'\u1EAF'), + 'abrevebelowdot': (0x1001eb7, u'\u1EB7'), + 'abrevegrave': (0x1001eb1, u'\u1EB1'), + 'abrevehook': (0x1001eb3, u'\u1EB3'), + 'abrevetilde': (0x1001eb5, u'\u1EB5'), + 'acircumflex': (0x00e2, u'\u00E2'), + 'acircumflexacute': (0x1001ea5, u'\u1EA5'), + 'acircumflexbelowdot': (0x1001ead, u'\u1EAD'), + 'acircumflexgrave': (0x1001ea7, u'\u1EA7'), + 'acircumflexhook': (0x1001ea9, u'\u1EA9'), + 'acircumflextilde': (0x1001eab, u'\u1EAB'), + 'acute': (0x00b4, u'\u00B4'), + 'adiaeresis': (0x00e4, u'\u00E4'), + 'ae': (0x00e6, u'\u00E6'), + 'agrave': (0x00e0, u'\u00E0'), + 'ahook': (0x1001ea3, u'\u1EA3'), + 'amacron': (0x03e0, u'\u0101'), + 'ampersand': (0x0026, u'\u0026'), + 'aogonek': (0x01b1, u'\u0105'), + 'apostrophe': (0x0027, u'\u0027'), + 'approxeq': (0x1002248, u'\u2245'), + 'approximate': (0x08c8, u'\u223C'), + 'aring': (0x00e5, u'\u00E5'), + 'asciicircum': (0x005e, u'\u005E'), + 'asciitilde': (0x007e, u'\u007E'), + 'asterisk': (0x002a, u'\u002A'), + 'at': (0x0040, u'\u0040'), + 'atilde': (0x00e3, u'\u00E3'), + 'b': (0x0062, u'\u0062'), + 'babovedot': (0x1001e03, u'\u1E03'), + 'backslash': (0x005c, u'\u005C'), + 'ballotcross': (0x0af4, u'\u2717'), + 'bar': (0x007c, u'\u007C'), + 'because': (0x1002235, u'\u2235'), + 'botintegral': (0x08a5, u'\u2321'), + 'botleftparens': (0x08ac, u'\u239D'), + 'botleftsqbracket': (0x08a8, u'\u23A3'), + 'botrightparens': (0x08ae, u'\u23A0'), + 'botrightsqbracket': (0x08aa, u'\u23A6'), + 'bott': (0x09f6, u'\u2534'), + 'braceleft': (0x007b, u'\u007B'), + 'braceright': (0x007d, u'\u007D'), + 'bracketleft': (0x005b, u'\u005B'), + 'bracketright': (0x005d, u'\u005D'), + 'braille_blank': (0x1002800, u'\u2800'), + 'braille_dots_1': (0x1002801, u'\u2801'), + 'braille_dots_12': (0x1002803, u'\u2803'), + 'braille_dots_123': (0x1002807, u'\u2807'), + 'braille_dots_1234': (0x100280f, u'\u280f'), + 'braille_dots_12345': (0x100281f, u'\u281f'), + 'braille_dots_123456': (0x100283f, u'\u283f'), + 'braille_dots_1234567': (0x100287f, u'\u287f'), + 'braille_dots_12345678': (0x10028ff, u'\u28ff'), + 'braille_dots_1234568': (0x10028bf, u'\u28bf'), + 'braille_dots_123457': (0x100285f, u'\u285f'), + 'braille_dots_1234578': (0x10028df, u'\u28df'), + 'braille_dots_123458': (0x100289f, u'\u289f'), + 'braille_dots_12346': (0x100282f, u'\u282f'), + 'braille_dots_123467': (0x100286f, u'\u286f'), + 'braille_dots_1234678': (0x10028ef, u'\u28ef'), + 'braille_dots_123468': (0x10028af, u'\u28af'), + 'braille_dots_12347': (0x100284f, u'\u284f'), + 'braille_dots_123478': (0x10028cf, u'\u28cf'), + 'braille_dots_12348': (0x100288f, u'\u288f'), + 'braille_dots_1235': (0x1002817, u'\u2817'), + 'braille_dots_12356': (0x1002837, u'\u2837'), + 'braille_dots_123567': (0x1002877, u'\u2877'), + 'braille_dots_1235678': (0x10028f7, u'\u28f7'), + 'braille_dots_123568': (0x10028b7, u'\u28b7'), + 'braille_dots_12357': (0x1002857, u'\u2857'), + 'braille_dots_123578': (0x10028d7, u'\u28d7'), + 'braille_dots_12358': (0x1002897, u'\u2897'), + 'braille_dots_1236': (0x1002827, u'\u2827'), + 'braille_dots_12367': (0x1002867, u'\u2867'), + 'braille_dots_123678': (0x10028e7, u'\u28e7'), + 'braille_dots_12368': (0x10028a7, u'\u28a7'), + 'braille_dots_1237': (0x1002847, u'\u2847'), + 'braille_dots_12378': (0x10028c7, u'\u28c7'), + 'braille_dots_1238': (0x1002887, u'\u2887'), + 'braille_dots_124': (0x100280b, u'\u280b'), + 'braille_dots_1245': (0x100281b, u'\u281b'), + 'braille_dots_12456': (0x100283b, u'\u283b'), + 'braille_dots_124567': (0x100287b, u'\u287b'), + 'braille_dots_1245678': (0x10028fb, u'\u28fb'), + 'braille_dots_124568': (0x10028bb, u'\u28bb'), + 'braille_dots_12457': (0x100285b, u'\u285b'), + 'braille_dots_124578': (0x10028db, u'\u28db'), + 'braille_dots_12458': (0x100289b, u'\u289b'), + 'braille_dots_1246': (0x100282b, u'\u282b'), + 'braille_dots_12467': (0x100286b, u'\u286b'), + 'braille_dots_124678': (0x10028eb, u'\u28eb'), + 'braille_dots_12468': (0x10028ab, u'\u28ab'), + 'braille_dots_1247': (0x100284b, u'\u284b'), + 'braille_dots_12478': (0x10028cb, u'\u28cb'), + 'braille_dots_1248': (0x100288b, u'\u288b'), + 'braille_dots_125': (0x1002813, u'\u2813'), + 'braille_dots_1256': (0x1002833, u'\u2833'), + 'braille_dots_12567': (0x1002873, u'\u2873'), + 'braille_dots_125678': (0x10028f3, u'\u28f3'), + 'braille_dots_12568': (0x10028b3, u'\u28b3'), + 'braille_dots_1257': (0x1002853, u'\u2853'), + 'braille_dots_12578': (0x10028d3, u'\u28d3'), + 'braille_dots_1258': (0x1002893, u'\u2893'), + 'braille_dots_126': (0x1002823, u'\u2823'), + 'braille_dots_1267': (0x1002863, u'\u2863'), + 'braille_dots_12678': (0x10028e3, u'\u28e3'), + 'braille_dots_1268': (0x10028a3, u'\u28a3'), + 'braille_dots_127': (0x1002843, u'\u2843'), + 'braille_dots_1278': (0x10028c3, u'\u28c3'), + 'braille_dots_128': (0x1002883, u'\u2883'), + 'braille_dots_13': (0x1002805, u'\u2805'), + 'braille_dots_134': (0x100280d, u'\u280d'), + 'braille_dots_1345': (0x100281d, u'\u281d'), + 'braille_dots_13456': (0x100283d, u'\u283d'), + 'braille_dots_134567': (0x100287d, u'\u287d'), + 'braille_dots_1345678': (0x10028fd, u'\u28fd'), + 'braille_dots_134568': (0x10028bd, u'\u28bd'), + 'braille_dots_13457': (0x100285d, u'\u285d'), + 'braille_dots_134578': (0x10028dd, u'\u28dd'), + 'braille_dots_13458': (0x100289d, u'\u289d'), + 'braille_dots_1346': (0x100282d, u'\u282d'), + 'braille_dots_13467': (0x100286d, u'\u286d'), + 'braille_dots_134678': (0x10028ed, u'\u28ed'), + 'braille_dots_13468': (0x10028ad, u'\u28ad'), + 'braille_dots_1347': (0x100284d, u'\u284d'), + 'braille_dots_13478': (0x10028cd, u'\u28cd'), + 'braille_dots_1348': (0x100288d, u'\u288d'), + 'braille_dots_135': (0x1002815, u'\u2815'), + 'braille_dots_1356': (0x1002835, u'\u2835'), + 'braille_dots_13567': (0x1002875, u'\u2875'), + 'braille_dots_135678': (0x10028f5, u'\u28f5'), + 'braille_dots_13568': (0x10028b5, u'\u28b5'), + 'braille_dots_1357': (0x1002855, u'\u2855'), + 'braille_dots_13578': (0x10028d5, u'\u28d5'), + 'braille_dots_1358': (0x1002895, u'\u2895'), + 'braille_dots_136': (0x1002825, u'\u2825'), + 'braille_dots_1367': (0x1002865, u'\u2865'), + 'braille_dots_13678': (0x10028e5, u'\u28e5'), + 'braille_dots_1368': (0x10028a5, u'\u28a5'), + 'braille_dots_137': (0x1002845, u'\u2845'), + 'braille_dots_1378': (0x10028c5, u'\u28c5'), + 'braille_dots_138': (0x1002885, u'\u2885'), + 'braille_dots_14': (0x1002809, u'\u2809'), + 'braille_dots_145': (0x1002819, u'\u2819'), + 'braille_dots_1456': (0x1002839, u'\u2839'), + 'braille_dots_14567': (0x1002879, u'\u2879'), + 'braille_dots_145678': (0x10028f9, u'\u28f9'), + 'braille_dots_14568': (0x10028b9, u'\u28b9'), + 'braille_dots_1457': (0x1002859, u'\u2859'), + 'braille_dots_14578': (0x10028d9, u'\u28d9'), + 'braille_dots_1458': (0x1002899, u'\u2899'), + 'braille_dots_146': (0x1002829, u'\u2829'), + 'braille_dots_1467': (0x1002869, u'\u2869'), + 'braille_dots_14678': (0x10028e9, u'\u28e9'), + 'braille_dots_1468': (0x10028a9, u'\u28a9'), + 'braille_dots_147': (0x1002849, u'\u2849'), + 'braille_dots_1478': (0x10028c9, u'\u28c9'), + 'braille_dots_148': (0x1002889, u'\u2889'), + 'braille_dots_15': (0x1002811, u'\u2811'), + 'braille_dots_156': (0x1002831, u'\u2831'), + 'braille_dots_1567': (0x1002871, u'\u2871'), + 'braille_dots_15678': (0x10028f1, u'\u28f1'), + 'braille_dots_1568': (0x10028b1, u'\u28b1'), + 'braille_dots_157': (0x1002851, u'\u2851'), + 'braille_dots_1578': (0x10028d1, u'\u28d1'), + 'braille_dots_158': (0x1002891, u'\u2891'), + 'braille_dots_16': (0x1002821, u'\u2821'), + 'braille_dots_167': (0x1002861, u'\u2861'), + 'braille_dots_1678': (0x10028e1, u'\u28e1'), + 'braille_dots_168': (0x10028a1, u'\u28a1'), + 'braille_dots_17': (0x1002841, u'\u2841'), + 'braille_dots_178': (0x10028c1, u'\u28c1'), + 'braille_dots_18': (0x1002881, u'\u2881'), + 'braille_dots_2': (0x1002802, u'\u2802'), + 'braille_dots_23': (0x1002806, u'\u2806'), + 'braille_dots_234': (0x100280e, u'\u280e'), + 'braille_dots_2345': (0x100281e, u'\u281e'), + 'braille_dots_23456': (0x100283e, u'\u283e'), + 'braille_dots_234567': (0x100287e, u'\u287e'), + 'braille_dots_2345678': (0x10028fe, u'\u28fe'), + 'braille_dots_234568': (0x10028be, u'\u28be'), + 'braille_dots_23457': (0x100285e, u'\u285e'), + 'braille_dots_234578': (0x10028de, u'\u28de'), + 'braille_dots_23458': (0x100289e, u'\u289e'), + 'braille_dots_2346': (0x100282e, u'\u282e'), + 'braille_dots_23467': (0x100286e, u'\u286e'), + 'braille_dots_234678': (0x10028ee, u'\u28ee'), + 'braille_dots_23468': (0x10028ae, u'\u28ae'), + 'braille_dots_2347': (0x100284e, u'\u284e'), + 'braille_dots_23478': (0x10028ce, u'\u28ce'), + 'braille_dots_2348': (0x100288e, u'\u288e'), + 'braille_dots_235': (0x1002816, u'\u2816'), + 'braille_dots_2356': (0x1002836, u'\u2836'), + 'braille_dots_23567': (0x1002876, u'\u2876'), + 'braille_dots_235678': (0x10028f6, u'\u28f6'), + 'braille_dots_23568': (0x10028b6, u'\u28b6'), + 'braille_dots_2357': (0x1002856, u'\u2856'), + 'braille_dots_23578': (0x10028d6, u'\u28d6'), + 'braille_dots_2358': (0x1002896, u'\u2896'), + 'braille_dots_236': (0x1002826, u'\u2826'), + 'braille_dots_2367': (0x1002866, u'\u2866'), + 'braille_dots_23678': (0x10028e6, u'\u28e6'), + 'braille_dots_2368': (0x10028a6, u'\u28a6'), + 'braille_dots_237': (0x1002846, u'\u2846'), + 'braille_dots_2378': (0x10028c6, u'\u28c6'), + 'braille_dots_238': (0x1002886, u'\u2886'), + 'braille_dots_24': (0x100280a, u'\u280a'), + 'braille_dots_245': (0x100281a, u'\u281a'), + 'braille_dots_2456': (0x100283a, u'\u283a'), + 'braille_dots_24567': (0x100287a, u'\u287a'), + 'braille_dots_245678': (0x10028fa, u'\u28fa'), + 'braille_dots_24568': (0x10028ba, u'\u28ba'), + 'braille_dots_2457': (0x100285a, u'\u285a'), + 'braille_dots_24578': (0x10028da, u'\u28da'), + 'braille_dots_2458': (0x100289a, u'\u289a'), + 'braille_dots_246': (0x100282a, u'\u282a'), + 'braille_dots_2467': (0x100286a, u'\u286a'), + 'braille_dots_24678': (0x10028ea, u'\u28ea'), + 'braille_dots_2468': (0x10028aa, u'\u28aa'), + 'braille_dots_247': (0x100284a, u'\u284a'), + 'braille_dots_2478': (0x10028ca, u'\u28ca'), + 'braille_dots_248': (0x100288a, u'\u288a'), + 'braille_dots_25': (0x1002812, u'\u2812'), + 'braille_dots_256': (0x1002832, u'\u2832'), + 'braille_dots_2567': (0x1002872, u'\u2872'), + 'braille_dots_25678': (0x10028f2, u'\u28f2'), + 'braille_dots_2568': (0x10028b2, u'\u28b2'), + 'braille_dots_257': (0x1002852, u'\u2852'), + 'braille_dots_2578': (0x10028d2, u'\u28d2'), + 'braille_dots_258': (0x1002892, u'\u2892'), + 'braille_dots_26': (0x1002822, u'\u2822'), + 'braille_dots_267': (0x1002862, u'\u2862'), + 'braille_dots_2678': (0x10028e2, u'\u28e2'), + 'braille_dots_268': (0x10028a2, u'\u28a2'), + 'braille_dots_27': (0x1002842, u'\u2842'), + 'braille_dots_278': (0x10028c2, u'\u28c2'), + 'braille_dots_28': (0x1002882, u'\u2882'), + 'braille_dots_3': (0x1002804, u'\u2804'), + 'braille_dots_34': (0x100280c, u'\u280c'), + 'braille_dots_345': (0x100281c, u'\u281c'), + 'braille_dots_3456': (0x100283c, u'\u283c'), + 'braille_dots_34567': (0x100287c, u'\u287c'), + 'braille_dots_345678': (0x10028fc, u'\u28fc'), + 'braille_dots_34568': (0x10028bc, u'\u28bc'), + 'braille_dots_3457': (0x100285c, u'\u285c'), + 'braille_dots_34578': (0x10028dc, u'\u28dc'), + 'braille_dots_3458': (0x100289c, u'\u289c'), + 'braille_dots_346': (0x100282c, u'\u282c'), + 'braille_dots_3467': (0x100286c, u'\u286c'), + 'braille_dots_34678': (0x10028ec, u'\u28ec'), + 'braille_dots_3468': (0x10028ac, u'\u28ac'), + 'braille_dots_347': (0x100284c, u'\u284c'), + 'braille_dots_3478': (0x10028cc, u'\u28cc'), + 'braille_dots_348': (0x100288c, u'\u288c'), + 'braille_dots_35': (0x1002814, u'\u2814'), + 'braille_dots_356': (0x1002834, u'\u2834'), + 'braille_dots_3567': (0x1002874, u'\u2874'), + 'braille_dots_35678': (0x10028f4, u'\u28f4'), + 'braille_dots_3568': (0x10028b4, u'\u28b4'), + 'braille_dots_357': (0x1002854, u'\u2854'), + 'braille_dots_3578': (0x10028d4, u'\u28d4'), + 'braille_dots_358': (0x1002894, u'\u2894'), + 'braille_dots_36': (0x1002824, u'\u2824'), + 'braille_dots_367': (0x1002864, u'\u2864'), + 'braille_dots_3678': (0x10028e4, u'\u28e4'), + 'braille_dots_368': (0x10028a4, u'\u28a4'), + 'braille_dots_37': (0x1002844, u'\u2844'), + 'braille_dots_378': (0x10028c4, u'\u28c4'), + 'braille_dots_38': (0x1002884, u'\u2884'), + 'braille_dots_4': (0x1002808, u'\u2808'), + 'braille_dots_45': (0x1002818, u'\u2818'), + 'braille_dots_456': (0x1002838, u'\u2838'), + 'braille_dots_4567': (0x1002878, u'\u2878'), + 'braille_dots_45678': (0x10028f8, u'\u28f8'), + 'braille_dots_4568': (0x10028b8, u'\u28b8'), + 'braille_dots_457': (0x1002858, u'\u2858'), + 'braille_dots_4578': (0x10028d8, u'\u28d8'), + 'braille_dots_458': (0x1002898, u'\u2898'), + 'braille_dots_46': (0x1002828, u'\u2828'), + 'braille_dots_467': (0x1002868, u'\u2868'), + 'braille_dots_4678': (0x10028e8, u'\u28e8'), + 'braille_dots_468': (0x10028a8, u'\u28a8'), + 'braille_dots_47': (0x1002848, u'\u2848'), + 'braille_dots_478': (0x10028c8, u'\u28c8'), + 'braille_dots_48': (0x1002888, u'\u2888'), + 'braille_dots_5': (0x1002810, u'\u2810'), + 'braille_dots_56': (0x1002830, u'\u2830'), + 'braille_dots_567': (0x1002870, u'\u2870'), + 'braille_dots_5678': (0x10028f0, u'\u28f0'), + 'braille_dots_568': (0x10028b0, u'\u28b0'), + 'braille_dots_57': (0x1002850, u'\u2850'), + 'braille_dots_578': (0x10028d0, u'\u28d0'), + 'braille_dots_58': (0x1002890, u'\u2890'), + 'braille_dots_6': (0x1002820, u'\u2820'), + 'braille_dots_67': (0x1002860, u'\u2860'), + 'braille_dots_678': (0x10028e0, u'\u28e0'), + 'braille_dots_68': (0x10028a0, u'\u28a0'), + 'braille_dots_7': (0x1002840, u'\u2840'), + 'braille_dots_78': (0x10028c0, u'\u28c0'), + 'braille_dots_8': (0x1002880, u'\u2880'), + 'breve': (0x01a2, u'\u02D8'), + 'brokenbar': (0x00a6, u'\u00A6'), + 'c': (0x0063, u'\u0063'), + 'cabovedot': (0x02e5, u'\u010B'), + 'cacute': (0x01e6, u'\u0107'), + 'careof': (0x0ab8, u'\u2105'), + 'caret': (0x0afc, u'\u2038'), + 'caron': (0x01b7, u'\u02C7'), + 'ccaron': (0x01e8, u'\u010D'), + 'ccedilla': (0x00e7, u'\u00E7'), + 'ccircumflex': (0x02e6, u'\u0109'), + 'cedilla': (0x00b8, u'\u00B8'), + 'cent': (0x00a2, u'\u00A2'), + 'checkerboard': (0x09e1, u'\u2592'), + 'checkmark': (0x0af3, u'\u2713'), + 'circle': (0x0bcf, u'\u25CB'), + 'club': (0x0aec, u'\u2663'), + 'colon': (0x003a, u'\u003A'), + 'comma': (0x002c, u'\u002C'), + 'containsas': (0x100220B, u'\u220B'), + 'copyright': (0x00a9, u'\u00A9'), + 'cr': (0x09e4, u'\u240D'), + 'crossinglines': (0x09ee, u'\u253C'), + 'cuberoot': (0x100221B, u'\u221B'), + 'currency': (0x00a4, u'\u00A4'), + 'd': (0x0064, u'\u0064'), + 'dabovedot': (0x1001e0b, u'\u1E0B'), + 'dagger': (0x0af1, u'\u2020'), + 'dcaron': (0x01ef, u'\u010F'), + 'dead_A': (0xfe81, None), + 'dead_E': (0xfe83, None), + 'dead_I': (0xfe85, None), + 'dead_O': (0xfe87, None), + 'dead_U': (0xfe89, None), + 'dead_a': (0xfe80, None), + 'dead_abovecomma': (0xfe64, u'\u0315'), + 'dead_abovedot': (0xfe56, u'\u0307'), + 'dead_abovereversedcomma': (0xfe65, u'\u0312'), + 'dead_abovering': (0xfe58, u'\u030A'), + 'dead_aboveverticalline': (0xfe91, u'\u030D'), + 'dead_acute': (0xfe51, u'\u0301'), + 'dead_belowbreve': (0xfe6b, u'\u032E'), + 'dead_belowcircumflex': (0xfe69, u'\u032D'), + 'dead_belowcomma': (0xfe6e, u'\u0326'), + 'dead_belowdiaeresis': (0xfe6c, u'\u0324'), + 'dead_belowdot': (0xfe60, u'\u0323'), + 'dead_belowmacron': (0xfe68, u'\u0331'), + 'dead_belowring': (0xfe67, u'\u0325'), + 'dead_belowtilde': (0xfe6a, u'\u0330'), + 'dead_belowverticalline': (0xfe92, u'\u0329'), + 'dead_breve': (0xfe55, u'\u0306'), + 'dead_capital_schwa': (0xfe8b, None), + 'dead_caron': (0xfe5a, u'\u030C'), + 'dead_cedilla': (0xfe5b, u'\u0327'), + 'dead_circumflex': (0xfe52, u'\u0302'), + 'dead_currency': (0xfe6f, None), + 'dead_diaeresis': (0xfe57, u'\u0308'), + 'dead_doubleacute': (0xfe59, u'\u030B'), + 'dead_doublegrave': (0xfe66, u'\u030F'), + 'dead_e': (0xfe82, None), + 'dead_grave': (0xfe50, u'\u0300'), + 'dead_greek': (0xfe8c, None), + 'dead_hook': (0xfe61, u'\u0309'), + 'dead_horn': (0xfe62, u'\u031B'), + 'dead_i': (0xfe84, None), + 'dead_invertedbreve': (0xfe6d, u'\u032F'), + 'dead_iota': (0xfe5d, u'\u0345'), + 'dead_longsolidusoverlay': (0xfe93, u'\u0338'), + 'dead_lowline': (0xfe90, u'\u0332'), + 'dead_macron': (0xfe54, u'\u0304'), + 'dead_o': (0xfe86, None), + 'dead_ogonek': (0xfe5c, u'\u0328'), + 'dead_semivoiced_sound': (0xfe5f, None), + 'dead_small_schwa': (0xfe8a, None), + 'dead_stroke': (0xfe63, u'\u0335'), + 'dead_tilde': (0xfe53, u'\u0303'), + 'dead_u': (0xfe88, None), + 'dead_voiced_sound': (0xfe5e, None), + 'degree': (0x00b0, u'\u00B0'), + 'diaeresis': (0x00a8, u'\u00A8'), + 'diamond': (0x0aed, u'\u2666'), + 'digitspace': (0x0aa5, u'\u2007'), + 'dintegral': (0x100222C, u'\u222C'), + 'division': (0x00f7, u'\u00F7'), + 'dollar': (0x0024, u'\u0024'), + 'doubbaselinedot': (0x0aaf, u'\u2025'), + 'doubleacute': (0x01bd, u'\u02DD'), + 'doubledagger': (0x0af2, u'\u2021'), + 'doublelowquotemark': (0x0afe, u'\u201E'), + 'downarrow': (0x08fe, u'\u2193'), + 'downstile': (0x0bc4, u'\u230A'), + 'downtack': (0x0bc2, u'\u22A4'), + 'dstroke': (0x01f0, u'\u0111'), + 'e': (0x0065, u'\u0065'), + 'eabovedot': (0x03ec, u'\u0117'), + 'eacute': (0x00e9, u'\u00E9'), + 'ebelowdot': (0x1001eb9, u'\u1EB9'), + 'ecaron': (0x01ec, u'\u011B'), + 'ecircumflex': (0x00ea, u'\u00EA'), + 'ecircumflexacute': (0x1001ebf, u'\u1EBF'), + 'ecircumflexbelowdot': (0x1001ec7, u'\u1EC7'), + 'ecircumflexgrave': (0x1001ec1, u'\u1EC1'), + 'ecircumflexhook': (0x1001ec3, u'\u1EC3'), + 'ecircumflextilde': (0x1001ec5, u'\u1EC5'), + 'ediaeresis': (0x00eb, u'\u00EB'), + 'egrave': (0x00e8, u'\u00E8'), + 'ehook': (0x1001ebb, u'\u1EBB'), + 'eightsubscript': (0x1002088, u'\u2088'), + 'eightsuperior': (0x1002078, u'\u2078'), + 'elementof': (0x1002208, u'\u2208'), + 'ellipsis': (0x0aae, u'\u2026'), + 'em3space': (0x0aa3, u'\u2004'), + 'em4space': (0x0aa4, u'\u2005'), + 'emacron': (0x03ba, u'\u0113'), + 'emdash': (0x0aa9, u'\u2014'), + 'emptyset': (0x1002205, u'\u2205'), + 'emspace': (0x0aa1, u'\u2003'), + 'endash': (0x0aaa, u'\u2013'), + 'eng': (0x03bf, u'\u014B'), + 'enspace': (0x0aa2, u'\u2002'), + 'eogonek': (0x01ea, u'\u0119'), + 'equal': (0x003d, u'\u003D'), + 'eth': (0x00f0, u'\u00F0'), + 'etilde': (0x1001ebd, u'\u1EBD'), + 'exclam': (0x0021, u'\u0021'), + 'exclamdown': (0x00a1, u'\u00A1'), + 'ezh': (0x1000292, u'\u0292'), + 'f': (0x0066, u'\u0066'), + 'fabovedot': (0x1001e1f, u'\u1E1F'), + 'femalesymbol': (0x0af8, u'\u2640'), + 'ff': (0x09e3, u'\u240C'), + 'figdash': (0x0abb, u'\u2012'), + 'fiveeighths': (0x0ac5, u'\u215D'), + 'fivesixths': (0x0ab7, u'\u215A'), + 'fivesubscript': (0x1002085, u'\u2085'), + 'fivesuperior': (0x1002075, u'\u2075'), + 'fourfifths': (0x0ab5, u'\u2158'), + 'foursubscript': (0x1002084, u'\u2084'), + 'foursuperior': (0x1002074, u'\u2074'), + 'fourthroot': (0x100221C, u'\u221C'), + 'function': (0x08f6, u'\u0192'), + 'g': (0x0067, u'\u0067'), + 'gabovedot': (0x02f5, u'\u0121'), + 'gbreve': (0x02bb, u'\u011F'), + 'gcaron': (0x10001e7, u'\u01E7'), + 'gcedilla': (0x03bb, u'\u0123'), + 'gcircumflex': (0x02f8, u'\u011D'), + 'grave': (0x0060, u'\u0060'), + 'greater': (0x003e, u'\u003E'), + 'greaterthanequal': (0x08be, u'\u2265'), + 'guillemotleft': (0x00ab, u'\u00AB'), + 'guillemotright': (0x00bb, u'\u00BB'), + 'h': (0x0068, u'\u0068'), + 'hairspace': (0x0aa8, u'\u200A'), + 'hcircumflex': (0x02b6, u'\u0125'), + 'heart': (0x0aee, u'\u2665'), + 'hebrew_aleph': (0x0ce0, u'\u05D0'), + 'hebrew_ayin': (0x0cf2, u'\u05E2'), + 'hebrew_bet': (0x0ce1, u'\u05D1'), + 'hebrew_chet': (0x0ce7, u'\u05D7'), + 'hebrew_dalet': (0x0ce3, u'\u05D3'), + 'hebrew_doublelowline': (0x0cdf, u'\u2017'), + 'hebrew_finalkaph': (0x0cea, u'\u05DA'), + 'hebrew_finalmem': (0x0ced, u'\u05DD'), + 'hebrew_finalnun': (0x0cef, u'\u05DF'), + 'hebrew_finalpe': (0x0cf3, u'\u05E3'), + 'hebrew_finalzade': (0x0cf5, u'\u05E5'), + 'hebrew_gimel': (0x0ce2, u'\u05D2'), + 'hebrew_he': (0x0ce4, u'\u05D4'), + 'hebrew_kaph': (0x0ceb, u'\u05DB'), + 'hebrew_lamed': (0x0cec, u'\u05DC'), + 'hebrew_mem': (0x0cee, u'\u05DE'), + 'hebrew_nun': (0x0cf0, u'\u05E0'), + 'hebrew_pe': (0x0cf4, u'\u05E4'), + 'hebrew_qoph': (0x0cf7, u'\u05E7'), + 'hebrew_resh': (0x0cf8, u'\u05E8'), + 'hebrew_samech': (0x0cf1, u'\u05E1'), + 'hebrew_shin': (0x0cf9, u'\u05E9'), + 'hebrew_taw': (0x0cfa, u'\u05EA'), + 'hebrew_tet': (0x0ce8, u'\u05D8'), + 'hebrew_waw': (0x0ce5, u'\u05D5'), + 'hebrew_yod': (0x0ce9, u'\u05D9'), + 'hebrew_zade': (0x0cf6, u'\u05E6'), + 'hebrew_zain': (0x0ce6, u'\u05D6'), + 'horizlinescan1': (0x09ef, u'\u23BA'), + 'horizlinescan3': (0x09f0, u'\u23BB'), + 'horizlinescan5': (0x09f1, u'\u2500'), + 'horizlinescan7': (0x09f2, u'\u23BC'), + 'horizlinescan9': (0x09f3, u'\u23BD'), + 'hstroke': (0x02b1, u'\u0127'), + 'ht': (0x09e2, u'\u2409'), + 'hyphen': (0x00ad, u'\u00AD'), + 'i': (0x0069, u'\u0069'), + 'iacute': (0x00ed, u'\u00ED'), + 'ibelowdot': (0x1001ecb, u'\u1ECB'), + 'ibreve': (0x100012d, u'\u012D'), + 'icircumflex': (0x00ee, u'\u00EE'), + 'identical': (0x08cf, u'\u2261'), + 'idiaeresis': (0x00ef, u'\u00EF'), + 'idotless': (0x02b9, u'\u0131'), + 'ifonlyif': (0x08cd, u'\u21D4'), + 'igrave': (0x00ec, u'\u00EC'), + 'ihook': (0x1001ec9, u'\u1EC9'), + 'imacron': (0x03ef, u'\u012B'), + 'implies': (0x08ce, u'\u21D2'), + 'includedin': (0x08da, u'\u2282'), + 'includes': (0x08db, u'\u2283'), + 'infinity': (0x08c2, u'\u221E'), + 'integral': (0x08bf, u'\u222B'), + 'intersection': (0x08dc, u'\u2229'), + 'iogonek': (0x03e7, u'\u012F'), + 'itilde': (0x03b5, u'\u0129'), + 'j': (0x006a, u'\u006A'), + 'jcircumflex': (0x02bc, u'\u0135'), + 'jot': (0x0bca, u'\u2218'), + 'k': (0x006b, u'\u006B'), + 'kana_A': (0x04b1, u'\u30A2'), + 'kana_CHI': (0x04c1, u'\u30C1'), + 'kana_E': (0x04b4, u'\u30A8'), + 'kana_FU': (0x04cc, u'\u30D5'), + 'kana_HA': (0x04ca, u'\u30CF'), + 'kana_HE': (0x04cd, u'\u30D8'), + 'kana_HI': (0x04cb, u'\u30D2'), + 'kana_HO': (0x04ce, u'\u30DB'), + 'kana_I': (0x04b2, u'\u30A4'), + 'kana_KA': (0x04b6, u'\u30AB'), + 'kana_KE': (0x04b9, u'\u30B1'), + 'kana_KI': (0x04b7, u'\u30AD'), + 'kana_KO': (0x04ba, u'\u30B3'), + 'kana_KU': (0x04b8, u'\u30AF'), + 'kana_MA': (0x04cf, u'\u30DE'), + 'kana_ME': (0x04d2, u'\u30E1'), + 'kana_MI': (0x04d0, u'\u30DF'), + 'kana_MO': (0x04d3, u'\u30E2'), + 'kana_MU': (0x04d1, u'\u30E0'), + 'kana_N': (0x04dd, u'\u30F3'), + 'kana_NA': (0x04c5, u'\u30CA'), + 'kana_NE': (0x04c8, u'\u30CD'), + 'kana_NI': (0x04c6, u'\u30CB'), + 'kana_NO': (0x04c9, u'\u30CE'), + 'kana_NU': (0x04c7, u'\u30CC'), + 'kana_O': (0x04b5, u'\u30AA'), + 'kana_RA': (0x04d7, u'\u30E9'), + 'kana_RE': (0x04da, u'\u30EC'), + 'kana_RI': (0x04d8, u'\u30EA'), + 'kana_RO': (0x04db, u'\u30ED'), + 'kana_RU': (0x04d9, u'\u30EB'), + 'kana_SA': (0x04bb, u'\u30B5'), + 'kana_SE': (0x04be, u'\u30BB'), + 'kana_SHI': (0x04bc, u'\u30B7'), + 'kana_SO': (0x04bf, u'\u30BD'), + 'kana_SU': (0x04bd, u'\u30B9'), + 'kana_TA': (0x04c0, u'\u30BF'), + 'kana_TE': (0x04c3, u'\u30C6'), + 'kana_TO': (0x04c4, u'\u30C8'), + 'kana_TSU': (0x04c2, u'\u30C4'), + 'kana_U': (0x04b3, u'\u30A6'), + 'kana_WA': (0x04dc, u'\u30EF'), + 'kana_WO': (0x04a6, u'\u30F2'), + 'kana_YA': (0x04d4, u'\u30E4'), + 'kana_YO': (0x04d6, u'\u30E8'), + 'kana_YU': (0x04d5, u'\u30E6'), + 'kana_a': (0x04a7, u'\u30A1'), + 'kana_closingbracket': (0x04a3, u'\u300D'), + 'kana_comma': (0x04a4, u'\u3001'), + 'kana_conjunctive': (0x04a5, u'\u30FB'), + 'kana_e': (0x04aa, u'\u30A7'), + 'kana_fullstop': (0x04a1, u'\u3002'), + 'kana_i': (0x04a8, u'\u30A3'), + 'kana_o': (0x04ab, u'\u30A9'), + 'kana_openingbracket': (0x04a2, u'\u300C'), + 'kana_tsu': (0x04af, u'\u30C3'), + 'kana_u': (0x04a9, u'\u30A5'), + 'kana_ya': (0x04ac, u'\u30E3'), + 'kana_yo': (0x04ae, u'\u30E7'), + 'kana_yu': (0x04ad, u'\u30E5'), + 'kcedilla': (0x03f3, u'\u0137'), + 'kra': (0x03a2, u'\u0138'), + 'l': (0x006c, u'\u006C'), + 'lacute': (0x01e5, u'\u013A'), + 'latincross': (0x0ad9, u'\u271D'), + 'lbelowdot': (0x1001e37, u'\u1E37'), + 'lcaron': (0x01b5, u'\u013E'), + 'lcedilla': (0x03b6, u'\u013C'), + 'leftarrow': (0x08fb, u'\u2190'), + 'leftdoublequotemark': (0x0ad2, u'\u201C'), + 'leftmiddlecurlybrace': (0x08af, u'\u23A8'), + 'leftradical': (0x08a1, u'\u23B7'), + 'leftsinglequotemark': (0x0ad0, u'\u2018'), + 'leftt': (0x09f4, u'\u251C'), + 'lefttack': (0x0bdc, u'\u22A3'), + 'less': (0x003c, u'\u003C'), + 'lessthanequal': (0x08bc, u'\u2264'), + 'lf': (0x09e5, u'\u240A'), + 'logicaland': (0x08de, u'\u2227'), + 'logicalor': (0x08df, u'\u2228'), + 'lowleftcorner': (0x09ed, u'\u2514'), + 'lowrightcorner': (0x09ea, u'\u2518'), + 'lstroke': (0x01b3, u'\u0142'), + 'm': (0x006d, u'\u006D'), + 'mabovedot': (0x1001e41, u'\u1E41'), + 'macron': (0x00af, u'\u00AF'), + 'malesymbol': (0x0af7, u'\u2642'), + 'maltesecross': (0x0af0, u'\u2720'), + 'masculine': (0x00ba, u'\u00BA'), + 'minus': (0x002d, u'\u002D'), + 'minutes': (0x0ad6, u'\u2032'), + 'mu': (0x00b5, u'\u00B5'), + 'multiply': (0x00d7, u'\u00D7'), + 'musicalflat': (0x0af6, u'\u266D'), + 'musicalsharp': (0x0af5, u'\u266F'), + 'n': (0x006e, u'\u006E'), + 'nabla': (0x08c5, u'\u2207'), + 'nacute': (0x01f1, u'\u0144'), + 'ncaron': (0x01f2, u'\u0148'), + 'ncedilla': (0x03f1, u'\u0146'), + 'ninesubscript': (0x1002089, u'\u2089'), + 'ninesuperior': (0x1002079, u'\u2079'), + 'nl': (0x09e8, u'\u2424'), + 'nobreakspace': (0x00a0, u'\u00A0'), + 'notapproxeq': (0x1002247, u'\u2247'), + 'notelementof': (0x1002209, u'\u2209'), + 'notequal': (0x08bd, u'\u2260'), + 'notidentical': (0x1002262, u'\u2262'), + 'notsign': (0x00ac, u'\u00AC'), + 'ntilde': (0x00f1, u'\u00F1'), + 'numbersign': (0x0023, u'\u0023'), + 'numerosign': (0x06b0, u'\u2116'), + 'o': (0x006f, u'\u006F'), + 'oacute': (0x00f3, u'\u00F3'), + 'obarred': (0x1000275, u'\u0275'), + 'obelowdot': (0x1001ecd, u'\u1ECD'), + 'ocaron': (0x10001d2, u'\u01D2'), + 'ocircumflex': (0x00f4, u'\u00F4'), + 'ocircumflexacute': (0x1001ed1, u'\u1ED1'), + 'ocircumflexbelowdot': (0x1001ed9, u'\u1ED9'), + 'ocircumflexgrave': (0x1001ed3, u'\u1ED3'), + 'ocircumflexhook': (0x1001ed5, u'\u1ED5'), + 'ocircumflextilde': (0x1001ed7, u'\u1ED7'), + 'odiaeresis': (0x00f6, u'\u00F6'), + 'odoubleacute': (0x01f5, u'\u0151'), + 'oe': (0x13bd, u'\u0153'), + 'ogonek': (0x01b2, u'\u02DB'), + 'ograve': (0x00f2, u'\u00F2'), + 'ohook': (0x1001ecf, u'\u1ECF'), + 'ohorn': (0x10001a1, u'\u01A1'), + 'ohornacute': (0x1001edb, u'\u1EDB'), + 'ohornbelowdot': (0x1001ee3, u'\u1EE3'), + 'ohorngrave': (0x1001edd, u'\u1EDD'), + 'ohornhook': (0x1001edf, u'\u1EDF'), + 'ohorntilde': (0x1001ee1, u'\u1EE1'), + 'omacron': (0x03f2, u'\u014D'), + 'oneeighth': (0x0ac3, u'\u215B'), + 'onefifth': (0x0ab2, u'\u2155'), + 'onehalf': (0x00bd, u'\u00BD'), + 'onequarter': (0x00bc, u'\u00BC'), + 'onesixth': (0x0ab6, u'\u2159'), + 'onesubscript': (0x1002081, u'\u2081'), + 'onesuperior': (0x00b9, u'\u00B9'), + 'onethird': (0x0ab0, u'\u2153'), + 'ooblique': (0x00f8, u'\u00F8'), + 'ordfeminine': (0x00aa, u'\u00AA'), + 'oslash': (0x00f8, u'\u00F8'), + 'otilde': (0x00f5, u'\u00F5'), + 'overline': (0x047e, u'\u203E'), + 'p': (0x0070, u'\u0070'), + 'pabovedot': (0x1001e57, u'\u1E57'), + 'paragraph': (0x00b6, u'\u00B6'), + 'parenleft': (0x0028, u'\u0028'), + 'parenright': (0x0029, u'\u0029'), + 'partdifferential': (0x1002202, u'\u2202'), + 'partialderivative': (0x08ef, u'\u2202'), + 'percent': (0x0025, u'\u0025'), + 'period': (0x002e, u'\u002E'), + 'periodcentered': (0x00b7, u'\u00B7'), + 'permille': (0x0ad5, u'\u2030'), + 'phonographcopyright': (0x0afb, u'\u2117'), + 'plus': (0x002b, u'\u002B'), + 'plusminus': (0x00b1, u'\u00B1'), + 'prescription': (0x0ad4, u'\u211E'), + 'prolongedsound': (0x04b0, u'\u30FC'), + 'punctspace': (0x0aa6, u'\u2008'), + 'q': (0x0071, u'\u0071'), + 'quad': (0x0bcc, u'\u2395'), + 'question': (0x003f, u'\u003F'), + 'questiondown': (0x00bf, u'\u00BF'), + 'quotedbl': (0x0022, u'\u0022'), + 'r': (0x0072, u'\u0072'), + 'racute': (0x01e0, u'\u0155'), + 'radical': (0x08d6, u'\u221A'), + 'rcaron': (0x01f8, u'\u0159'), + 'rcedilla': (0x03b3, u'\u0157'), + 'registered': (0x00ae, u'\u00AE'), + 'rightarrow': (0x08fd, u'\u2192'), + 'rightdoublequotemark': (0x0ad3, u'\u201D'), + 'rightmiddlecurlybrace': (0x08b0, u'\u23AC'), + 'rightsinglequotemark': (0x0ad1, u'\u2019'), + 'rightt': (0x09f5, u'\u2524'), + 'righttack': (0x0bfc, u'\u22A2'), + 's': (0x0073, u'\u0073'), + 'sabovedot': (0x1001e61, u'\u1E61'), + 'sacute': (0x01b6, u'\u015B'), + 'scaron': (0x01b9, u'\u0161'), + 'scedilla': (0x01ba, u'\u015F'), + 'schwa': (0x1000259, u'\u0259'), + 'scircumflex': (0x02fe, u'\u015D'), + 'seconds': (0x0ad7, u'\u2033'), + 'section': (0x00a7, u'\u00A7'), + 'semicolon': (0x003b, u'\u003B'), + 'semivoicedsound': (0x04df, u'\u309C'), + 'seveneighths': (0x0ac6, u'\u215E'), + 'sevensubscript': (0x1002087, u'\u2087'), + 'sevensuperior': (0x1002077, u'\u2077'), + 'similarequal': (0x08c9, u'\u2243'), + 'singlelowquotemark': (0x0afd, u'\u201A'), + 'sixsubscript': (0x1002086, u'\u2086'), + 'sixsuperior': (0x1002076, u'\u2076'), + 'slash': (0x002f, u'\u002F'), + 'soliddiamond': (0x09e0, u'\u25C6'), + 'space': (0x0020, u'\u0020'), + 'squareroot': (0x100221A, u'\u221A'), + 'ssharp': (0x00df, u'\u00DF'), + 'sterling': (0x00a3, u'\u00A3'), + 'stricteq': (0x1002263, u'\u2263'), + 't': (0x0074, u'\u0074'), + 'tabovedot': (0x1001e6b, u'\u1E6B'), + 'tcaron': (0x01bb, u'\u0165'), + 'tcedilla': (0x01fe, u'\u0163'), + 'telephone': (0x0af9, u'\u260E'), + 'telephonerecorder': (0x0afa, u'\u2315'), + 'therefore': (0x08c0, u'\u2234'), + 'thinspace': (0x0aa7, u'\u2009'), + 'thorn': (0x00fe, u'\u00FE'), + 'threeeighths': (0x0ac4, u'\u215C'), + 'threefifths': (0x0ab4, u'\u2157'), + 'threequarters': (0x00be, u'\u00BE'), + 'threesubscript': (0x1002083, u'\u2083'), + 'threesuperior': (0x00b3, u'\u00B3'), + 'tintegral': (0x100222D, u'\u222D'), + 'topintegral': (0x08a4, u'\u2320'), + 'topleftparens': (0x08ab, u'\u239B'), + 'topleftsqbracket': (0x08a7, u'\u23A1'), + 'toprightparens': (0x08ad, u'\u239E'), + 'toprightsqbracket': (0x08a9, u'\u23A4'), + 'topt': (0x09f7, u'\u252C'), + 'trademark': (0x0ac9, u'\u2122'), + 'tslash': (0x03bc, u'\u0167'), + 'twofifths': (0x0ab3, u'\u2156'), + 'twosubscript': (0x1002082, u'\u2082'), + 'twosuperior': (0x00b2, u'\u00B2'), + 'twothirds': (0x0ab1, u'\u2154'), + 'u': (0x0075, u'\u0075'), + 'uacute': (0x00fa, u'\u00FA'), + 'ubelowdot': (0x1001ee5, u'\u1EE5'), + 'ubreve': (0x02fd, u'\u016D'), + 'ucircumflex': (0x00fb, u'\u00FB'), + 'udiaeresis': (0x00fc, u'\u00FC'), + 'udoubleacute': (0x01fb, u'\u0171'), + 'ugrave': (0x00f9, u'\u00F9'), + 'uhook': (0x1001ee7, u'\u1EE7'), + 'uhorn': (0x10001b0, u'\u01B0'), + 'uhornacute': (0x1001ee9, u'\u1EE9'), + 'uhornbelowdot': (0x1001ef1, u'\u1EF1'), + 'uhorngrave': (0x1001eeb, u'\u1EEB'), + 'uhornhook': (0x1001eed, u'\u1EED'), + 'uhorntilde': (0x1001eef, u'\u1EEF'), + 'umacron': (0x03fe, u'\u016B'), + 'underscore': (0x005f, u'\u005F'), + 'union': (0x08dd, u'\u222A'), + 'uogonek': (0x03f9, u'\u0173'), + 'uparrow': (0x08fc, u'\u2191'), + 'upleftcorner': (0x09ec, u'\u250C'), + 'uprightcorner': (0x09eb, u'\u2510'), + 'upstile': (0x0bd3, u'\u2308'), + 'uptack': (0x0bce, u'\u22A5'), + 'uring': (0x01f9, u'\u016F'), + 'utilde': (0x03fd, u'\u0169'), + 'v': (0x0076, u'\u0076'), + 'variation': (0x08c1, u'\u221D'), + 'vertbar': (0x09f8, u'\u2502'), + 'voicedsound': (0x04de, u'\u309B'), + 'vt': (0x09e9, u'\u240B'), + 'w': (0x0077, u'\u0077'), + 'wacute': (0x1001e83, u'\u1E83'), + 'wcircumflex': (0x1000175, u'\u0175'), + 'wdiaeresis': (0x1001e85, u'\u1E85'), + 'wgrave': (0x1001e81, u'\u1E81'), + 'x': (0x0078, u'\u0078'), + 'xabovedot': (0x1001e8b, u'\u1E8B'), + 'y': (0x0079, u'\u0079'), + 'yacute': (0x00fd, u'\u00FD'), + 'ybelowdot': (0x1001ef5, u'\u1EF5'), + 'ycircumflex': (0x1000177, u'\u0177'), + 'ydiaeresis': (0x00ff, u'\u00FF'), + 'yen': (0x00a5, u'\u00A5'), + 'ygrave': (0x1001ef3, u'\u1EF3'), + 'yhook': (0x1001ef7, u'\u1EF7'), + 'ytilde': (0x1001ef9, u'\u1EF9'), + 'z': (0x007a, u'\u007A'), + 'zabovedot': (0x01bf, u'\u017C'), + 'zacute': (0x01bc, u'\u017A'), + 'zcaron': (0x01be, u'\u017E'), + 'zerosubscript': (0x1002080, u'\u2080'), + 'zerosuperior': (0x1002070, u'\u2070'), + 'zstroke': (0x10001b6, u'\u01B6')} + +DEAD_KEYS = { + u'\u0307': u'\u02D9', + u'\u030A': u'\u02DA', + u'\u0301': u'\u00B4', + u'\u0306': u'\u02D8', + u'\u030C': u'\u02C7', + u'\u0327': u'\u00B8', + u'\u0302': u'\u005E', + u'\u0308': u'\u00A8', + u'\u030B': u'\u02DD', + u'\u0300': u'\u0060', + u'\u0345': u'\u037A', + u'\u0332': u'\u005F', + u'\u0304': u'\u00AF', + u'\u0328': u'\u02DB', + u'\u0303': u'\u007E'} + +KEYPAD_KEYS = { + 'KP_0': 0xffb0, + 'KP_1': 0xffb1, + 'KP_2': 0xffb2, + 'KP_3': 0xffb3, + 'KP_4': 0xffb4, + 'KP_5': 0xffb5, + 'KP_6': 0xffb6, + 'KP_7': 0xffb7, + 'KP_8': 0xffb8, + 'KP_9': 0xffb9, + 'KP_Add': 0xffab, + 'KP_Begin': 0xff9d, + 'KP_Decimal': 0xffae, + 'KP_Delete': 0xff9f, + 'KP_Divide': 0xffaf, + 'KP_Down': 0xff99, + 'KP_End': 0xff9c, + 'KP_Enter': 0xff8d, + 'KP_Equal': 0xffbd, + 'KP_F1': 0xff91, + 'KP_F2': 0xff92, + 'KP_F3': 0xff93, + 'KP_F4': 0xff94, + 'KP_Home': 0xff95, + 'KP_Insert': 0xff9e, + 'KP_Left': 0xff96, + 'KP_Multiply': 0xffaa, + 'KP_Next': 0xff9b, + 'KP_Page_Down': 0xff9b, + 'KP_Page_Up': 0xff9a, + 'KP_Prior': 0xff9a, + 'KP_Right': 0xff98, + 'KP_Separator': 0xffac, + 'KP_Space': 0xff80, + 'KP_Subtract': 0xffad, + 'KP_Tab': 0xff89, + 'KP_Up': 0xff97} + +CHARS = { + codepoint: name + for name, (keysym, codepoint) in SYMBOLS.items() + if codepoint} + +KEYSYMS = { + keysym: name + for name, (keysym, codepoint) in SYMBOLS.items() + if codepoint} diff --git a/pynput/mouse/__init__.py b/pynput/mouse/__init__.py new file mode 100644 index 0000000..4acce3a --- /dev/null +++ b/pynput/mouse/__init__.py @@ -0,0 +1,56 @@ +# coding=utf-8 +# pynput +# Copyright (C) 2015-2018 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +The module containing mouse classes. + +See the documentation for more information. +""" + +# pylint: disable=C0103 +# Button, Controller and Listener are not constants + +import os +import sys + +if os.environ.get('__PYNPUT_GENERATE_DOCUMENTATION') == 'yes': + from ._base import Button, Controller, Listener +else: + Button = None + Controller = None + Listener = None + + +if sys.platform == 'darwin': + if not Button and not Controller and not Listener: + from ._darwin import Button, Controller, Listener + +elif sys.platform == 'win32': + if not Button and not Controller and not Listener: + from ._win32 import Button, Controller, Listener + +else: + if not Button and not Controller and not Listener: + try: + from ._xorg import Button, Controller, Listener + except ImportError: + # For now, since we only support Xlib anyway, we re-raise these + # errors to allow users to determine the cause of failures to import + raise + + +if not Button or not Controller or not Listener: + raise ImportError('this platform is not supported') diff --git a/pynput/mouse/__pycache__/__init__.cpython-36.pyc b/pynput/mouse/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000..cf85f8a Binary files /dev/null and b/pynput/mouse/__pycache__/__init__.cpython-36.pyc differ diff --git a/pynput/mouse/__pycache__/_base.cpython-36.pyc b/pynput/mouse/__pycache__/_base.cpython-36.pyc new file mode 100644 index 0000000..22b9d30 Binary files /dev/null and b/pynput/mouse/__pycache__/_base.cpython-36.pyc differ diff --git a/pynput/mouse/__pycache__/_win32.cpython-36.pyc b/pynput/mouse/__pycache__/_win32.cpython-36.pyc new file mode 100644 index 0000000..1218919 Binary files /dev/null and b/pynput/mouse/__pycache__/_win32.cpython-36.pyc differ diff --git a/pynput/mouse/__pycache__/_xorg.cpython-36.pyc b/pynput/mouse/__pycache__/_xorg.cpython-36.pyc new file mode 100644 index 0000000..00e1d5f Binary files /dev/null and b/pynput/mouse/__pycache__/_xorg.cpython-36.pyc differ diff --git a/pynput/mouse/_base.py b/pynput/mouse/_base.py new file mode 100644 index 0000000..acd4e4a --- /dev/null +++ b/pynput/mouse/_base.py @@ -0,0 +1,263 @@ +# coding=utf-8 +# pynput +# Copyright (C) 2015-2018 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +This module contains the base implementation. + +The actual interface to mouse classes is defined here, but the implementation +is located in a platform dependent module. +""" + +# pylint: disable=R0903 +# We implement stubs + +import enum + +from pynput._util import AbstractListener +from pynput import _logger + + +class Button(enum.Enum): + """The various buttons. + + The actual values for these items differ between platforms. Some + platforms may have additional buttons, but these are guaranteed to be + present everywhere. + """ + #: An unknown button was pressed + unknown = 0 + + #: The left button + left = 1 + + #: The middle button + middle = 2 + + #: The right button + right = 3 + + +class Controller(object): + """A controller for sending virtual mouse events to the system. + """ + def __init__(self): + self._log = _logger(self.__class__) + + @property + def position(self): + """The current position of the mouse pointer. + + This is the tuple ``(x, y)``, and setting it will move the pointer. + """ + return self._position_get() + + @position.setter + def position(self, pos): + self._position_set(pos) + + def scroll(self, dx, dy): + """Sends scroll events. + + :param int dx: The horizontal scroll. The units of scrolling is + undefined. + + :param int dy: The vertical scroll. The units of scrolling is + undefined. + + :raises ValueError: if the values are invalid, for example out of + bounds + """ + self._scroll(dx, dy) + + def press(self, button): + """Emits a button press event at the current position. + + :param Button button: The button to press. + """ + self._press(button) + + def release(self, button): + """Emits a button release event at the current position. + + :param Button button: The button to release. + """ + self._release(button) + + def move(self, dx, dy): + """Moves the mouse pointer a number of pixels from its current + position. + + :param int x: The horizontal offset. + + :param int dy: The vertical offset. + + :raises ValueError: if the values are invalid, for example out of + bounds + """ + self.position = tuple(sum(i) for i in zip(self.position, (dx, dy))) + + def click(self, button, count=1): + """Emits a button click event at the current position. + + The default implementation sends a series of press and release events. + + :param Button button: The button to click. + + :param int count: The number of clicks to send. + """ + with self as controller: + for _ in range(count): + controller.press(button) + controller.release(button) + + def __enter__(self): + """Begins a series of clicks. + + In the default :meth:`click` implementation, the return value of this + method is used for the calls to :meth:`press` and :meth:`release` + instead of ``self``. + + The default implementation is a no-op. + """ + return self + + def __exit__(self, exc_type, value, traceback): + """Ends a series of clicks. + """ + pass + + def _position_get(self): + """The implementation of the getter for :attr:`position`. + + This is a platform dependent implementation. + """ + raise NotImplementedError() + + def _position_set(self, pos): + """The implementation of the setter for :attr:`position`. + + This is a platform dependent implementation. + """ + raise NotImplementedError() + + def _scroll(self, dx, dy): + """The implementation of the :meth:`scroll` method. + + This is a platform dependent implementation. + """ + raise NotImplementedError() + + def _press(self, button): + """The implementation of the :meth:`press` method. + + This is a platform dependent implementation. + """ + raise NotImplementedError() + + def _release(self, button): + """The implementation of the :meth:`release` method. + + This is a platform dependent implementation. + """ + raise NotImplementedError() + + +# pylint: disable=W0223; This is also an abstract class +class Listener(AbstractListener): + """A listener for mouse events. + + Instances of this class can be used as context managers. This is equivalent + to the following code:: + + listener.start() + try: + listener.wait() + with_statements() + finally: + listener.stop() + + This class inherits from :class:`threading.Thread` and supports all its + methods. It will set :attr:`daemon` to ``True`` when created. + + :param callable on_move: The callback to call when mouse move events occur. + + It will be called with the arguments ``(x, y)``, which is the new + pointer position. If this callback raises :class:`StopException` or + returns ``False``, the listener is stopped. + + :param callable on_click: The callback to call when a mouse button is + clicked. + + It will be called with the arguments ``(x, y, button, pressed)``, + where ``(x, y)`` is the new pointer position, ``button`` is one of the + :class:`Button` values and ``pressed`` is whether the button was + pressed. + + If this callback raises :class:`StopException` or returns ``False``, + the listener is stopped. + + :param callable on_scroll: The callback to call when mouse scroll + events occur. + + It will be called with the arguments ``(x, y, dx, dy)``, where + ``(x, y)`` is the new pointer position, and ``(dx, dy)`` is the scroll + vector. + + If this callback raises :class:`StopException` or returns ``False``, + the listener is stopped. + + :param bool suppress: Whether to suppress events. Setting this to ``True`` + will prevent the input events from being passed to the rest of the + system. + + :param kwargs: Any non-standard platform dependent options. These should be + prefixed with the platform name thus: ``darwin_``, ``xorg_`` or + ``win32_``. + + Supported values are: + + ``darwin_intercept`` + A callable taking the arguments ``(event_type, event)``, where + ``event_type`` is any mouse related event type constant, and + ``event`` is a ``CGEventRef``. + + This callable can freely modify the event using functions like + ``Quartz.CGEventSetIntegerValueField``. If this callable does not + return the event, the event is suppressed system wide. + + ``win32_event_filter`` + A callable taking the arguments ``(msg, data)``, where ``msg`` is + the current message, and ``data`` associated data as a + `MSLLHOOKSTRUCT `_. + + If this callback returns ``False``, the event will not + be propagated to the listener callback. + + If ``self.suppress_event()`` is called, the event is suppressed + system wide. + """ + def __init__(self, on_move=None, on_click=None, on_scroll=None, + suppress=False, **kwargs): + self._log = _logger(self.__class__) + prefix = self.__class__.__module__.rsplit('.', 1)[-1][1:] + '_' + self._options = { + key[len(prefix):]: value + for key, value in kwargs.items() + if key.startswith(prefix)} + super(Listener, self).__init__( + on_move=on_move, on_click=on_click, on_scroll=on_scroll, + suppress=suppress) +# pylint: enable=W0223 diff --git a/pynput/mouse/_darwin.py b/pynput/mouse/_darwin.py new file mode 100644 index 0000000..369884a --- /dev/null +++ b/pynput/mouse/_darwin.py @@ -0,0 +1,215 @@ +# coding=utf-8 +# pynput +# Copyright (C) 2015-2018 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +The mouse implementation for *OSX*. +""" + +# pylint: disable=C0111 +# The documentation is extracted from the base classes + +# pylint: disable=R0903 +# We implement stubs + +import enum +import Quartz + +from AppKit import NSEvent + +from pynput._util.darwin import ( + ListenerMixin) +from . import _base + + +def _button_value(base_name, mouse_button): + """Generates the value tuple for a :class:`Button` value. + + :param str base_name: The base name for the button. This shuld be a string + like ``'kCGEventLeftMouse'``. + + :param int mouse_button: The mouse button ID. + + :return: a value tuple + """ + return ( + tuple( + getattr(Quartz, '%sMouse%s' % (base_name, name)) + for name in ('Down', 'Up', 'Dragged')), + mouse_button) + + +class Button(enum.Enum): + """The various buttons. + """ + unknown = None + left = _button_value('kCGEventLeft', 0) + middle = _button_value('kCGEventOther', 2) + right = _button_value('kCGEventRight', 1) + + +class Controller(_base.Controller): + #: The scroll speed + _SCROLL_SPEED = 5 + + def __init__(self, *args, **kwargs): + super(Controller, self).__init__(*args, **kwargs) + self._click = None + self._drag_button = None + + def _position_get(self): + pos = NSEvent.mouseLocation() + + return pos.x, Quartz.CGDisplayPixelsHigh(0) - pos.y + + def _position_set(self, pos): + try: + (_, _, mouse_type), mouse_button = self._drag_button.value + except AttributeError: + mouse_type = Quartz.kCGEventMouseMoved + mouse_button = 0 + + Quartz.CGEventPost( + Quartz.kCGHIDEventTap, + Quartz.CGEventCreateMouseEvent( + None, + mouse_type, + pos, + mouse_button)) + + def _scroll(self, dx, dy): + while dx != 0 or dy != 0: + xval = 1 if dx > 0 else -1 if dx < 0 else 0 + dx -= xval + yval = 1 if dy > 0 else -1 if dy < 0 else 0 + dy -= yval + + Quartz.CGEventPost( + Quartz.kCGHIDEventTap, + Quartz.CGEventCreateScrollWheelEvent( + None, + Quartz.kCGScrollEventUnitPixel, + 2, + yval * self._SCROLL_SPEED, + xval * self._SCROLL_SPEED)) + + def _press(self, button): + (press, _, _), mouse_button = button.value + event = Quartz.CGEventCreateMouseEvent( + None, + press, + self.position, + mouse_button) + + # If we are performing a click, we need to set this state flag + if self._click is not None: + self._click += 1 + Quartz.CGEventSetIntegerValueField( + event, + Quartz.kCGMouseEventClickState, + self._click) + + Quartz.CGEventPost(Quartz.kCGHIDEventTap, event) + + # Store the button to enable dragging + self._drag_button = button + + def _release(self, button): + (_, release, _), mouse_button = button.value + event = Quartz.CGEventCreateMouseEvent( + None, + release, + self.position, + mouse_button) + + # If we are performing a click, we need to set this state flag + if self._click is not None: + Quartz.CGEventSetIntegerValueField( + event, + Quartz.kCGMouseEventClickState, + self._click) + + Quartz.CGEventPost(Quartz.kCGHIDEventTap, event) + + if button == self._drag_button: + self._drag_button = None + + def __enter__(self): + self._click = 0 + return self + + def __exit__(self, exc_type, value, traceback): + self._click = None + + +class Listener(ListenerMixin, _base.Listener): + #: The events that we listen to + _EVENTS = ( + Quartz.CGEventMaskBit(Quartz.kCGEventMouseMoved) | + Quartz.CGEventMaskBit(Quartz.kCGEventLeftMouseDown) | + Quartz.CGEventMaskBit(Quartz.kCGEventLeftMouseUp) | + Quartz.CGEventMaskBit(Quartz.kCGEventLeftMouseDragged) | + Quartz.CGEventMaskBit(Quartz.kCGEventRightMouseDown) | + Quartz.CGEventMaskBit(Quartz.kCGEventRightMouseUp) | + Quartz.CGEventMaskBit(Quartz.kCGEventRightMouseDragged) | + Quartz.CGEventMaskBit(Quartz.kCGEventOtherMouseDown) | + Quartz.CGEventMaskBit(Quartz.kCGEventOtherMouseUp) | + Quartz.CGEventMaskBit(Quartz.kCGEventOtherMouseDragged) | + Quartz.CGEventMaskBit(Quartz.kCGEventScrollWheel)) + + def __init__(self, *args, **kwargs): + super(Listener, self).__init__(*args, **kwargs) + self._intercept = self._options.get( + 'intercept', + None) + + def _handle(self, dummy_proxy, event_type, event, dummy_refcon): + """The callback registered with *Mac OSX* for mouse events. + + This method will call the callbacks registered on initialisation. + """ + try: + (px, py) = Quartz.CGEventGetLocation(event) + except AttributeError: + # This happens during teardown of the virtual machine + return + + # Quickly detect the most common event type + if event_type == Quartz.kCGEventMouseMoved: + self.on_move(px, py) + + elif event_type == Quartz.kCGEventScrollWheel: + dx = Quartz.CGEventGetIntegerValueField( + event, + Quartz.kCGScrollWheelEventDeltaAxis2) + dy = Quartz.CGEventGetIntegerValueField( + event, + Quartz.kCGScrollWheelEventDeltaAxis1) + self.on_scroll(px, py, dx, dy) + + else: + for button in Button: + try: + (press, release, drag), _ = button.value + except TypeError: + # Button.unknown cannot be enumerated + continue + + # Press and release generate click events, and drag + # generates move events + if event_type in (press, release): + self.on_click(px, py, button, event_type == press) + elif event_type == drag: + self.on_move(px, py) diff --git a/pynput/mouse/_win32.py b/pynput/mouse/_win32.py new file mode 100644 index 0000000..a4d542a --- /dev/null +++ b/pynput/mouse/_win32.py @@ -0,0 +1,195 @@ +# coding=utf-8 +# pynput +# Copyright (C) 2015-2018 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +The mouse implementation for *Windows*. +""" + +# pylint: disable=C0111 +# The documentation is extracted from the base classes + +# pylint: disable=R0903 +# We implement stubs + +import ctypes +import enum + +from ctypes import ( + windll, + wintypes) + +from pynput._util import NotifierMixin +from pynput._util.win32 import ( + INPUT, + INPUT_union, + ListenerMixin, + MOUSEINPUT, + SendInput, + SystemHook) +from . import _base + + +class Button(enum.Enum): + """The various buttons. + """ + unknown = None + left = (MOUSEINPUT.LEFTUP, MOUSEINPUT.LEFTDOWN) + middle = (MOUSEINPUT.MIDDLEUP, MOUSEINPUT.MIDDLEDOWN) + right = (MOUSEINPUT.RIGHTUP, MOUSEINPUT.RIGHTDOWN) + + +class Controller(NotifierMixin, _base.Controller): + __GetCursorPos = windll.user32.GetCursorPos + __SetCursorPos = windll.user32.SetCursorPos + + def __init__(self, *args, **kwargs): + super(Controller, self).__init__(*args, **kwargs) + + def _position_get(self): + point = wintypes.POINT() + if self.__GetCursorPos(ctypes.byref(point)): + return (point.x, point.y) + else: + return None + + def _position_set(self, pos): + pos = int(pos[0]), int(pos[1]) + self.__SetCursorPos(*pos) + self._emit('on_move', *pos) + + def _scroll(self, dx, dy): + if dy: + SendInput( + 1, + ctypes.byref(INPUT( + type=INPUT.MOUSE, + value=INPUT_union( + mi=MOUSEINPUT( + dwFlags=MOUSEINPUT.WHEEL, + mouseData=int(dy))))), + ctypes.sizeof(INPUT)) + + if dx: + SendInput( + 1, + ctypes.byref(INPUT( + type=INPUT.MOUSE, + value=INPUT_union( + mi=MOUSEINPUT( + dwFlags=MOUSEINPUT.HWHEEL, + mouseData=int(dx))))), + ctypes.sizeof(INPUT)) + + if dx or dy: + px, py = self._position_get() + self._emit('on_scroll', px, py, dx, dy) + + def _press(self, button): + SendInput( + 1, + ctypes.byref(INPUT( + type=INPUT.MOUSE, + value=INPUT_union( + mi=MOUSEINPUT( + dwFlags=button.value[1])))), + ctypes.sizeof(INPUT)) + + def _release(self, button): + SendInput( + 1, + ctypes.byref(INPUT( + type=INPUT.MOUSE, + value=INPUT_union( + mi=MOUSEINPUT( + dwFlags=button.value[0])))), + ctypes.sizeof(INPUT)) + + +@Controller._receiver +class Listener(ListenerMixin, _base.Listener): + #: The Windows hook ID for low level mouse events, ``WH_MOUSE_LL`` + _EVENTS = 14 + + WM_LBUTTONDOWN = 0x0201 + WM_LBUTTONUP = 0x0202 + WM_MBUTTONDOWN = 0x0207 + WM_MBUTTONUP = 0x0208 + WM_MOUSEMOVE = 0x0200 + WM_MOUSEWHEEL = 0x020A + WM_MOUSEHWHEEL = 0x020E + WM_RBUTTONDOWN = 0x0204 + WM_RBUTTONUP = 0x0205 + + _WHEEL_DELTA = 120 + + #: A mapping from messages to button events + CLICK_BUTTONS = { + WM_LBUTTONDOWN: (Button.left, True), + WM_LBUTTONUP: (Button.left, False), + WM_MBUTTONDOWN: (Button.middle, True), + WM_MBUTTONUP: (Button.middle, False), + WM_RBUTTONDOWN: (Button.right, True), + WM_RBUTTONUP: (Button.right, False)} + + #: A mapping from messages to scroll vectors + SCROLL_BUTTONS = { + WM_MOUSEWHEEL: (0, 1), + WM_MOUSEHWHEEL: (1, 0)} + + _HANDLED_EXCEPTIONS = ( + SystemHook.SuppressException,) + + class _MSLLHOOKSTRUCT(ctypes.Structure): + """Contains information about a mouse event passed to a ``WH_MOUSE_LL`` + hook procedure, ``MouseProc``. + """ + _fields_ = [ + ('pt', wintypes.POINT), + ('mouseData', wintypes.DWORD), + ('flags', wintypes.DWORD), + ('time', wintypes.DWORD), + ('dwExtraInfo', ctypes.c_void_p)] + + #: A pointer to a :class:`_MSLLHOOKSTRUCT` + _LPMSLLHOOKSTRUCT = ctypes.POINTER(_MSLLHOOKSTRUCT) + + def __init__(self, *args, **kwargs): + super(Listener, self).__init__(*args, **kwargs) + self._event_filter = self._options.get( + 'event_filter', + lambda msg, data: True) + + def _handle(self, code, msg, lpdata): + if code != SystemHook.HC_ACTION: + return + + data = ctypes.cast(lpdata, self._LPMSLLHOOKSTRUCT).contents + + # Suppress further propagation of the event if it is filtered + if self._event_filter(msg, data) is False: + return + + if msg == self.WM_MOUSEMOVE: + self.on_move(data.pt.x, data.pt.y) + + elif msg in self.CLICK_BUTTONS: + button, pressed = self.CLICK_BUTTONS[msg] + self.on_click(data.pt.x, data.pt.y, button, pressed) + + elif msg in self.SCROLL_BUTTONS: + mx, my = self.SCROLL_BUTTONS[msg] + dd = wintypes.SHORT(data.mouseData >> 16).value // self._WHEEL_DELTA + self.on_scroll(data.pt.x, data.pt.y, dd * mx, dd * my) diff --git a/pynput/mouse/_xorg.py b/pynput/mouse/_xorg.py new file mode 100644 index 0000000..005f0bd --- /dev/null +++ b/pynput/mouse/_xorg.py @@ -0,0 +1,174 @@ +# coding=utf-8 +# pynput +# Copyright (C) 2015-2018 Moses Palmér +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +""" +The keyboard implementation for *Xorg*. +""" + +# pylint: disable=C0111 +# The documentation is extracted from the base classes + + +# pylint: disable=E1101,E1102 +# We dynamically generate the Button class + +# pylint: disable=R0903 +# We implement stubs + +import enum +import Xlib.display +import Xlib.ext +import Xlib.ext.xtest +import Xlib.X +import Xlib.protocol + +from pynput._util.xorg import ( + display_manager, + ListenerMixin) +from . import _base + + +# pylint: disable=C0103 +Button = enum.Enum( + 'Button', + module=__name__, + names=[ + ('unknown', None), + ('left', 1), + ('middle', 2), + ('right', 3), + ('scroll_up', 4), + ('scroll_down', 5), + ('scroll_left', 6), + ('scroll_right', 7)] + [ + ('button%d' % i, i) + for i in range(8, 31)]) +# pylint: enable=C0103 + + +class Controller(_base.Controller): + def __init__(self, *args, **kwargs): + super(Controller, self).__init__(*args, **kwargs) + self._display = Xlib.display.Display() + + def __del__(self): + if hasattr(self, '_display'): + self._display.close() + + def _position_get(self): + with display_manager(self._display) as dm: + qp = dm.screen().root.query_pointer() + return (qp.root_x, qp.root_y) + + def _position_set(self, pos): + px, py = self._check_bounds(*pos) + with display_manager(self._display) as dm: + Xlib.ext.xtest.fake_input(dm, Xlib.X.MotionNotify, x=px, y=py) + + def _scroll(self, dx, dy): + dx, dy = self._check_bounds(dx, dy) + if dy: + self.click( + button=Button.scroll_up if dy > 0 else Button.scroll_down, + count=abs(dy)) + + if dx: + self.click( + button=Button.scroll_right if dx > 0 else Button.scroll_left, + count=abs(dx)) + + def _press(self, button): + with display_manager(self._display) as dm: + Xlib.ext.xtest.fake_input(dm, Xlib.X.ButtonPress, button.value) + + def _release(self, button): + with display_manager(self._display) as dm: + Xlib.ext.xtest.fake_input(dm, Xlib.X.ButtonRelease, button.value) + + def _check_bounds(self, *args): + """Checks the arguments and makes sure they are within the bounds of a + short integer. + + :param args: The values to verify. + """ + if not all( + (-0x7fff - 1) <= number <= 0x7fff + for number in args): + raise ValueError(args) + else: + return tuple(int(p) for p in args) + + +class Listener(ListenerMixin, _base.Listener): + #: A mapping from button values to scroll directions + _SCROLL_BUTTONS = { + Button.scroll_up.value: (0, 1), + Button.scroll_down.value: (0, -1), + Button.scroll_right.value: (1, 0), + Button.scroll_left.value: (-1, 0)} + + _EVENTS = ( + Xlib.X.ButtonPressMask, + Xlib.X.ButtonReleaseMask) + + def __init__(self, *args, **kwargs): + super(Listener, self).__init__(*args, **kwargs) + + def _handle(self, dummy_display, event): + px = event.root_x + py = event.root_y + + if event.type == Xlib.X.ButtonPress: + # Scroll events are sent as button presses with the scroll + # button codes + scroll = self._SCROLL_BUTTONS.get(event.detail, None) + if scroll: + self.on_scroll(px, py, *scroll) + else: + self.on_click(px, py, self._button(event.detail), True) + + elif event.type == Xlib.X.ButtonRelease: + # Send an event only if this was not a scroll event + if event.detail not in self._SCROLL_BUTTONS: + self.on_click(px, py, self._button(event.detail), False) + + else: + self.on_move(px, py) + + + def _suppress_start(self, display): + display.screen().root.grab_pointer( + True, self._event_mask, Xlib.X.GrabModeAsync, Xlib.X.GrabModeAsync, + 0, 0, Xlib.X.CurrentTime) + + def _suppress_stop(self, display): + display.ungrab_pointer(Xlib.X.CurrentTime) + + # pylint: disable=R0201 + def _button(self, detail): + """Creates a mouse button from an event detail. + + If the button is unknown, :attr:`Button.unknown` is returned. + + :param detail: The event detail. + + :return: a button + """ + try: + return Button(detail) + except ValueError: + return Button.unknown + # pylint: enable=R0201 diff --git a/ui_commandeditwnd.py b/ui_commandeditwnd.py new file mode 100644 index 0000000..15baa19 --- /dev/null +++ b/ui_commandeditwnd.py @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'commandeditwnd.ui' +# +# Created by: PyQt5 UI code generator 5.12.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_CommandEditDialog(object): + def setupUi(self, CommandEditDialog): + CommandEditDialog.setObjectName("CommandEditDialog") + CommandEditDialog.resize(927, 419) + font = QtGui.QFont() + font.setPointSize(10) + CommandEditDialog.setFont(font) + self.gridLayout = QtWidgets.QGridLayout(CommandEditDialog) + self.gridLayout.setContentsMargins(25, 20, 20, 15) + self.gridLayout.setObjectName("gridLayout") + spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.gridLayout.addItem(spacerItem, 4, 0, 1, 2) + self.horizontalLayout_6 = QtWidgets.QHBoxLayout() + self.horizontalLayout_6.setObjectName("horizontalLayout_6") + self.label_2 = QtWidgets.QLabel(CommandEditDialog) + self.label_2.setObjectName("label_2") + self.horizontalLayout_6.addWidget(self.label_2) + spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_6.addItem(spacerItem1) + self.gridLayout.addLayout(self.horizontalLayout_6, 2, 0, 1, 2) + spacerItem2 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.gridLayout.addItem(spacerItem2, 1, 0, 1, 1) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.label = QtWidgets.QLabel(CommandEditDialog) + self.label.setObjectName("label") + self.horizontalLayout.addWidget(self.label) + self.say = QtWidgets.QLineEdit(CommandEditDialog) + self.say.setObjectName("say") + self.horizontalLayout.addWidget(self.say) + spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout.addItem(spacerItem3) + self.gridLayout.addLayout(self.horizontalLayout, 0, 0, 1, 8) + self.horizontalLayout_9 = QtWidgets.QHBoxLayout() + self.horizontalLayout_9.setObjectName("horizontalLayout_9") + self.oneExe = QtWidgets.QRadioButton(CommandEditDialog) + self.oneExe.setAutoExclusive(True) + self.oneExe.setObjectName("oneExe") + self.horizontalLayout_9.addWidget(self.oneExe) + spacerItem4 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_9.addItem(spacerItem4) + self.gridLayout.addLayout(self.horizontalLayout_9, 6, 0, 1, 1) + self.horizontalLayout_10 = QtWidgets.QHBoxLayout() + self.horizontalLayout_10.setObjectName("horizontalLayout_10") + self.continueExe = QtWidgets.QRadioButton(CommandEditDialog) + self.continueExe.setAutoExclusive(True) + self.continueExe.setObjectName("continueExe") + self.horizontalLayout_10.addWidget(self.continueExe) + spacerItem5 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_10.addItem(spacerItem5) + self.gridLayout.addLayout(self.horizontalLayout_10, 7, 0, 1, 1) + self.horizontalLayout_11 = QtWidgets.QHBoxLayout() + self.horizontalLayout_11.setObjectName("horizontalLayout_11") + self.repeatExe = QtWidgets.QRadioButton(CommandEditDialog) + self.repeatExe.setAutoExclusive(True) + self.repeatExe.setObjectName("repeatExe") + self.horizontalLayout_11.addWidget(self.repeatExe) + self.repeatCnt = QtWidgets.QSpinBox(CommandEditDialog) + self.repeatCnt.setMinimum(2) + self.repeatCnt.setObjectName("repeatCnt") + self.horizontalLayout_11.addWidget(self.repeatCnt) + self.label_3 = QtWidgets.QLabel(CommandEditDialog) + self.label_3.setObjectName("label_3") + self.horizontalLayout_11.addWidget(self.label_3) + spacerItem6 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_11.addItem(spacerItem6) + self.gridLayout.addLayout(self.horizontalLayout_11, 8, 0, 1, 1) + self.horizontalLayout_7 = QtWidgets.QHBoxLayout() + self.horizontalLayout_7.setObjectName("horizontalLayout_7") + self.verticalLayout_5 = QtWidgets.QVBoxLayout() + self.verticalLayout_5.setObjectName("verticalLayout_5") + self.keyBut = QtWidgets.QPushButton(CommandEditDialog) + self.keyBut.setAutoDefault(False) + self.keyBut.setObjectName("keyBut") + self.verticalLayout_5.addWidget(self.keyBut) + self.mouseBut = QtWidgets.QPushButton(CommandEditDialog) + self.mouseBut.setAutoDefault(False) + self.mouseBut.setObjectName("mouseBut") + self.verticalLayout_5.addWidget(self.mouseBut) + self.pauseBut = QtWidgets.QPushButton(CommandEditDialog) + self.pauseBut.setAutoDefault(False) + self.pauseBut.setObjectName("pauseBut") + self.verticalLayout_5.addWidget(self.pauseBut) + self.otherBut = QtWidgets.QPushButton(CommandEditDialog) + self.otherBut.setAutoDefault(False) + self.otherBut.setObjectName("otherBut") + self.verticalLayout_5.addWidget(self.otherBut) + self.horizontalLayout_7.addLayout(self.verticalLayout_5) + self.actionsListWidget = QtWidgets.QListWidget(CommandEditDialog) + font = QtGui.QFont() + font.setPointSize(11) + self.actionsListWidget.setFont(font) + self.actionsListWidget.setObjectName("actionsListWidget") + self.horizontalLayout_7.addWidget(self.actionsListWidget) + self.verticalLayout_6 = QtWidgets.QVBoxLayout() + self.verticalLayout_6.setObjectName("verticalLayout_6") + self.upBut = QtWidgets.QPushButton(CommandEditDialog) + self.upBut.setAutoDefault(False) + self.upBut.setObjectName("upBut") + self.verticalLayout_6.addWidget(self.upBut) + self.downBut = QtWidgets.QPushButton(CommandEditDialog) + self.downBut.setAutoDefault(False) + self.downBut.setObjectName("downBut") + self.verticalLayout_6.addWidget(self.downBut) + self.editBut = QtWidgets.QPushButton(CommandEditDialog) + self.editBut.setAutoDefault(False) + self.editBut.setObjectName("editBut") + self.verticalLayout_6.addWidget(self.editBut) + self.deleteBut = QtWidgets.QPushButton(CommandEditDialog) + self.deleteBut.setAutoDefault(False) + self.deleteBut.setObjectName("deleteBut") + self.verticalLayout_6.addWidget(self.deleteBut) + self.horizontalLayout_7.addLayout(self.verticalLayout_6) + self.gridLayout.addLayout(self.horizontalLayout_7, 3, 0, 1, 7) + self.horizontalLayout_8 = QtWidgets.QHBoxLayout() + self.horizontalLayout_8.setObjectName("horizontalLayout_8") + self.asyncChk = QtWidgets.QCheckBox(CommandEditDialog) + self.asyncChk.setChecked(True) + self.asyncChk.setObjectName("asyncChk") + self.horizontalLayout_8.addWidget(self.asyncChk) + spacerItem7 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_8.addItem(spacerItem7) + self.gridLayout.addLayout(self.horizontalLayout_8, 5, 0, 1, 3) + self.horizontalLayout_12 = QtWidgets.QHBoxLayout() + self.horizontalLayout_12.setSpacing(20) + self.horizontalLayout_12.setObjectName("horizontalLayout_12") + spacerItem8 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_12.addItem(spacerItem8) + self.ok = QtWidgets.QPushButton(CommandEditDialog) + self.ok.setMinimumSize(QtCore.QSize(130, 0)) + self.ok.setAutoDefault(False) + self.ok.setObjectName("ok") + self.horizontalLayout_12.addWidget(self.ok) + self.cancel = QtWidgets.QPushButton(CommandEditDialog) + self.cancel.setMinimumSize(QtCore.QSize(130, 0)) + self.cancel.setAutoDefault(False) + self.cancel.setObjectName("cancel") + self.horizontalLayout_12.addWidget(self.cancel) + self.gridLayout.addLayout(self.horizontalLayout_12, 12, 0, 1, 3) + + self.retranslateUi(CommandEditDialog) + QtCore.QMetaObject.connectSlotsByName(CommandEditDialog) + + def retranslateUi(self, CommandEditDialog): + _translate = QtCore.QCoreApplication.translate + CommandEditDialog.setWindowTitle(_translate("CommandEditDialog", "Command Edit Dialog")) + self.label_2.setText(_translate("CommandEditDialog", "When this command excutes, do the following sequence:")) + self.label.setText(_translate("CommandEditDialog", "When I say :")) + self.oneExe.setText(_translate("CommandEditDialog", "This command executes once")) + self.continueExe.setText(_translate("CommandEditDialog", "This command repeats continuously")) + self.repeatExe.setText(_translate("CommandEditDialog", "This command repeats")) + self.label_3.setText(_translate("CommandEditDialog", "times")) + self.keyBut.setText(_translate("CommandEditDialog", "Key Press")) + self.mouseBut.setText(_translate("CommandEditDialog", "Mouse")) + self.pauseBut.setText(_translate("CommandEditDialog", "Pause")) + self.otherBut.setText(_translate("CommandEditDialog", "Other")) + self.upBut.setText(_translate("CommandEditDialog", "Up")) + self.downBut.setText(_translate("CommandEditDialog", "Down")) + self.editBut.setText(_translate("CommandEditDialog", "Edit")) + self.deleteBut.setText(_translate("CommandEditDialog", "Delete")) + self.asyncChk.setText(_translate("CommandEditDialog", "Allow other commands to execute while this one is running")) + self.ok.setText(_translate("CommandEditDialog", "OK")) + self.cancel.setText(_translate("CommandEditDialog", "Cancel")) + + + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + CommandEditDialog = QtWidgets.QDialog() + ui = Ui_CommandEditDialog() + ui.setupUi(CommandEditDialog) + CommandEditDialog.show() + sys.exit(app.exec_()) diff --git a/ui_keyactioneditwnd.py b/ui_keyactioneditwnd.py new file mode 100644 index 0000000..e679be1 --- /dev/null +++ b/ui_keyactioneditwnd.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file '.\keyactioneditwnd.ui' +# +# Created by: PyQt5 UI code generator 5.12.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_KeyActionEditDialog(object): + def setupUi(self, KeyActionEditDialog): + KeyActionEditDialog.setObjectName("KeyActionEditDialog") + KeyActionEditDialog.resize(471, 164) + self.gridLayout = QtWidgets.QGridLayout(KeyActionEditDialog) + self.gridLayout.setObjectName("gridLayout") + self.label = QtWidgets.QLabel(KeyActionEditDialog) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 0, 0, 1, 1) + spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.gridLayout.addItem(spacerItem, 2, 0, 1, 1) + spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.gridLayout.addItem(spacerItem1, 4, 0, 1, 1) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setSpacing(20) + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.press_releaseKey = QtWidgets.QRadioButton(KeyActionEditDialog) + self.press_releaseKey.setObjectName("press_releaseKey") + self.horizontalLayout_2.addWidget(self.press_releaseKey) + self.pressKey = QtWidgets.QRadioButton(KeyActionEditDialog) + self.pressKey.setObjectName("pressKey") + self.horizontalLayout_2.addWidget(self.pressKey) + self.releaseKey = QtWidgets.QRadioButton(KeyActionEditDialog) + self.releaseKey.setObjectName("releaseKey") + self.horizontalLayout_2.addWidget(self.releaseKey) + self.gridLayout.addLayout(self.horizontalLayout_2, 3, 0, 1, 1) + self.horizontalLayout_3 = QtWidgets.QHBoxLayout() + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_3.addItem(spacerItem2) + self.ok = QtWidgets.QPushButton(KeyActionEditDialog) + self.ok.setAutoDefault(False) + self.ok.setObjectName("ok") + self.horizontalLayout_3.addWidget(self.ok) + self.cancel = QtWidgets.QPushButton(KeyActionEditDialog) + self.cancel.setAutoDefault(False) + self.cancel.setObjectName("cancel") + self.horizontalLayout_3.addWidget(self.cancel) + self.gridLayout.addLayout(self.horizontalLayout_3, 5, 0, 1, 1) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.ctrlBut = QtWidgets.QPushButton(KeyActionEditDialog) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + font.setStrikeOut(False) + self.ctrlBut.setFont(font) + self.ctrlBut.setAutoDefault(False) + self.ctrlBut.setObjectName("ctrlBut") + self.horizontalLayout.addWidget(self.ctrlBut) + self.altBut = QtWidgets.QPushButton(KeyActionEditDialog) + self.altBut.setAutoDefault(False) + self.altBut.setObjectName("altBut") + self.horizontalLayout.addWidget(self.altBut) + self.shiftBut = QtWidgets.QPushButton(KeyActionEditDialog) + self.shiftBut.setAutoDefault(False) + self.shiftBut.setObjectName("shiftBut") + self.horizontalLayout.addWidget(self.shiftBut) + self.winBut = QtWidgets.QPushButton(KeyActionEditDialog) + self.winBut.setAutoDefault(False) + self.winBut.setObjectName("winBut") + self.horizontalLayout.addWidget(self.winBut) + self.keyEdit = QtWidgets.QLineEdit(KeyActionEditDialog) + self.keyEdit.setAlignment(QtCore.Qt.AlignCenter) + self.keyEdit.setObjectName("keyEdit") + self.horizontalLayout.addWidget(self.keyEdit) + self.gridLayout.addLayout(self.horizontalLayout, 1, 0, 1, 1) + + self.retranslateUi(KeyActionEditDialog) + QtCore.QMetaObject.connectSlotsByName(KeyActionEditDialog) + + def retranslateUi(self, KeyActionEditDialog): + _translate = QtCore.QCoreApplication.translate + KeyActionEditDialog.setWindowTitle(_translate("KeyActionEditDialog", "Key Action Dialog")) + self.label.setText(_translate("KeyActionEditDialog", "Key (Combination):")) + self.press_releaseKey.setText(_translate("KeyActionEditDialog", "Press and Release Key(s)")) + self.pressKey.setText(_translate("KeyActionEditDialog", "Press Key(s)")) + self.releaseKey.setText(_translate("KeyActionEditDialog", "Release Key(s)")) + self.ok.setText(_translate("KeyActionEditDialog", "OK")) + self.cancel.setText(_translate("KeyActionEditDialog", "Cancel")) + self.ctrlBut.setText(_translate("KeyActionEditDialog", "Ctrl")) + self.altBut.setText(_translate("KeyActionEditDialog", "Alt")) + self.shiftBut.setText(_translate("KeyActionEditDialog", "Shift")) + self.winBut.setText(_translate("KeyActionEditDialog", "Win")) + + + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + KeyActionEditDialog = QtWidgets.QDialog() + ui = Ui_KeyActionEditDialog() + ui.setupUi(KeyActionEditDialog) + KeyActionEditDialog.show() + sys.exit(app.exec_()) diff --git a/ui_mainwnd.py b/ui_mainwnd.py new file mode 100644 index 0000000..84b0c0f --- /dev/null +++ b/ui_mainwnd.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'mainwnd.ui' +# +# Created by: PyQt5 UI code generator 5.12.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_MainWidget(object): + def setupUi(self, MainWidget): + MainWidget.setObjectName("MainWidget") + MainWidget.resize(820, 166) + font = QtGui.QFont() + font.setPointSize(10) + MainWidget.setFont(font) + self.gridLayout_2 = QtWidgets.QGridLayout(MainWidget) + self.gridLayout_2.setContentsMargins(-1, 20, -1, -1) + self.gridLayout_2.setVerticalSpacing(20) + self.gridLayout_2.setObjectName("gridLayout_2") + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setSpacing(20) + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.listeningChk = QtWidgets.QCheckBox(MainWidget) + self.listeningChk.setObjectName("listeningChk") + self.horizontalLayout_2.addWidget(self.listeningChk) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_2.addItem(spacerItem) + self.ok = QtWidgets.QPushButton(MainWidget) + self.ok.setMinimumSize(QtCore.QSize(130, 0)) + self.ok.setObjectName("ok") + self.horizontalLayout_2.addWidget(self.ok) + self.cancel = QtWidgets.QPushButton(MainWidget) + self.cancel.setMinimumSize(QtCore.QSize(130, 0)) + self.cancel.setObjectName("cancel") + self.horizontalLayout_2.addWidget(self.cancel) + self.gridLayout_2.addLayout(self.horizontalLayout_2, 1, 0, 1, 1) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setSpacing(20) + self.horizontalLayout.setObjectName("horizontalLayout") + self.label = QtWidgets.QLabel(MainWidget) + self.label.setMinimumSize(QtCore.QSize(60, 0)) + self.label.setObjectName("label") + self.horizontalLayout.addWidget(self.label) + self.profileCbx = QtWidgets.QComboBox(MainWidget) + self.profileCbx.setMinimumSize(QtCore.QSize(250, 0)) + self.profileCbx.setObjectName("profileCbx") + self.horizontalLayout.addWidget(self.profileCbx) + self.addBut = QtWidgets.QPushButton(MainWidget) + self.addBut.setMinimumSize(QtCore.QSize(130, 0)) + self.addBut.setObjectName("addBut") + self.horizontalLayout.addWidget(self.addBut) + self.editBut = QtWidgets.QPushButton(MainWidget) + self.editBut.setMinimumSize(QtCore.QSize(130, 0)) + self.editBut.setObjectName("editBut") + self.horizontalLayout.addWidget(self.editBut) + self.removeBut = QtWidgets.QPushButton(MainWidget) + self.removeBut.setMinimumSize(QtCore.QSize(130, 0)) + self.removeBut.setObjectName("removeBut") + self.horizontalLayout.addWidget(self.removeBut) + spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout.addItem(spacerItem1) + self.gridLayout_2.addLayout(self.horizontalLayout, 0, 0, 1, 1) + + self.retranslateUi(MainWidget) + QtCore.QMetaObject.connectSlotsByName(MainWidget) + + def retranslateUi(self, MainWidget): + _translate = QtCore.QCoreApplication.translate + MainWidget.setWindowTitle(_translate("MainWidget", "LinVAM")) + self.listeningChk.setText(_translate("MainWidget", "Enable Listening")) + self.ok.setText(_translate("MainWidget", "OK")) + self.cancel.setText(_translate("MainWidget", "Cancel")) + self.label.setText(_translate("MainWidget", "Profile:")) + self.addBut.setText(_translate("MainWidget", "Add")) + self.editBut.setText(_translate("MainWidget", "Edit")) + self.removeBut.setText(_translate("MainWidget", "Remove")) + + + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + MainWidget = QtWidgets.QWidget() + ui = Ui_MainWidget() + ui.setupUi(MainWidget) + MainWidget.show() + sys.exit(app.exec_()) diff --git a/ui_mouseactioneditwnd.py b/ui_mouseactioneditwnd.py new file mode 100644 index 0000000..6486466 --- /dev/null +++ b/ui_mouseactioneditwnd.py @@ -0,0 +1,218 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'mouseactioneditwnd.ui' +# +# Created by: PyQt5 UI code generator 5.12.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_MouseActionEditDialog(object): + def setupUi(self, MouseActionEditDialog): + MouseActionEditDialog.setObjectName("MouseActionEditDialog") + MouseActionEditDialog.resize(651, 268) + font = QtGui.QFont() + font.setPointSize(10) + MouseActionEditDialog.setFont(font) + self.gridLayout = QtWidgets.QGridLayout(MouseActionEditDialog) + self.gridLayout.setVerticalSpacing(20) + self.gridLayout.setObjectName("gridLayout") + self.mouseActionTabWidget = QtWidgets.QTabWidget(MouseActionEditDialog) + self.mouseActionTabWidget.setObjectName("mouseActionTabWidget") + self.clickActionTab = QtWidgets.QWidget() + self.clickActionTab.setObjectName("clickActionTab") + self.gridLayout_2 = QtWidgets.QGridLayout(self.clickActionTab) + self.gridLayout_2.setContentsMargins(-1, 20, -1, 9) + self.gridLayout_2.setObjectName("gridLayout_2") + self.leftClick = QtWidgets.QRadioButton(self.clickActionTab) + self.leftClick.setObjectName("leftClick") + self.gridLayout_2.addWidget(self.leftClick, 0, 0, 1, 1) + self.rightClick = QtWidgets.QRadioButton(self.clickActionTab) + self.rightClick.setObjectName("rightClick") + self.gridLayout_2.addWidget(self.rightClick, 0, 1, 1, 1) + self.middleClick = QtWidgets.QRadioButton(self.clickActionTab) + self.middleClick.setObjectName("middleClick") + self.gridLayout_2.addWidget(self.middleClick, 0, 2, 1, 1) + self.leftDclick = QtWidgets.QRadioButton(self.clickActionTab) + self.leftDclick.setObjectName("leftDclick") + self.gridLayout_2.addWidget(self.leftDclick, 1, 0, 1, 1) + self.rightDclick = QtWidgets.QRadioButton(self.clickActionTab) + self.rightDclick.setObjectName("rightDclick") + self.gridLayout_2.addWidget(self.rightDclick, 1, 1, 1, 1) + self.middleDclick = QtWidgets.QRadioButton(self.clickActionTab) + self.middleDclick.setObjectName("middleDclick") + self.gridLayout_2.addWidget(self.middleDclick, 1, 2, 1, 1) + self.leftDown = QtWidgets.QRadioButton(self.clickActionTab) + self.leftDown.setObjectName("leftDown") + self.gridLayout_2.addWidget(self.leftDown, 2, 0, 1, 1) + self.rightDown = QtWidgets.QRadioButton(self.clickActionTab) + self.rightDown.setObjectName("rightDown") + self.gridLayout_2.addWidget(self.rightDown, 2, 1, 1, 1) + self.middleDown = QtWidgets.QRadioButton(self.clickActionTab) + self.middleDown.setObjectName("middleDown") + self.gridLayout_2.addWidget(self.middleDown, 2, 2, 1, 1) + self.leftUp = QtWidgets.QRadioButton(self.clickActionTab) + self.leftUp.setObjectName("leftUp") + self.gridLayout_2.addWidget(self.leftUp, 3, 0, 1, 1) + self.rightUp = QtWidgets.QRadioButton(self.clickActionTab) + self.rightUp.setObjectName("rightUp") + self.gridLayout_2.addWidget(self.rightUp, 3, 1, 1, 1) + self.middleUp = QtWidgets.QRadioButton(self.clickActionTab) + self.middleUp.setObjectName("middleUp") + self.gridLayout_2.addWidget(self.middleUp, 3, 2, 1, 1) + self.mouseActionTabWidget.addTab(self.clickActionTab, "") + self.moveActionTab = QtWidgets.QWidget() + self.moveActionTab.setObjectName("moveActionTab") + self.gridLayout_3 = QtWidgets.QGridLayout(self.moveActionTab) + self.gridLayout_3.setContentsMargins(-1, 20, -1, -1) + self.gridLayout_3.setObjectName("gridLayout_3") + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.moveTo = QtWidgets.QRadioButton(self.moveActionTab) + self.moveTo.setMinimumSize(QtCore.QSize(100, 0)) + self.moveTo.setObjectName("moveTo") + self.horizontalLayout_2.addWidget(self.moveTo) + self.label_2 = QtWidgets.QLabel(self.moveActionTab) + self.label_2.setMinimumSize(QtCore.QSize(25, 0)) + self.label_2.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.label_2.setObjectName("label_2") + self.horizontalLayout_2.addWidget(self.label_2) + self.xEdit = QtWidgets.QLineEdit(self.moveActionTab) + self.xEdit.setObjectName("xEdit") + self.horizontalLayout_2.addWidget(self.xEdit) + self.label_3 = QtWidgets.QLabel(self.moveActionTab) + self.label_3.setMinimumSize(QtCore.QSize(25, 0)) + self.label_3.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.label_3.setObjectName("label_3") + self.horizontalLayout_2.addWidget(self.label_3) + self.yEdit = QtWidgets.QLineEdit(self.moveActionTab) + self.yEdit.setObjectName("yEdit") + self.horizontalLayout_2.addWidget(self.yEdit) + self.gridLayout_3.addLayout(self.horizontalLayout_2, 1, 0, 1, 3) + self.horizontalLayout_3 = QtWidgets.QHBoxLayout() + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.moveOffset = QtWidgets.QRadioButton(self.moveActionTab) + self.moveOffset.setMinimumSize(QtCore.QSize(100, 0)) + self.moveOffset.setAutoExclusive(True) + self.moveOffset.setObjectName("moveOffset") + self.horizontalLayout_3.addWidget(self.moveOffset) + self.label_5 = QtWidgets.QLabel(self.moveActionTab) + self.label_5.setMinimumSize(QtCore.QSize(25, 0)) + self.label_5.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.label_5.setObjectName("label_5") + self.horizontalLayout_3.addWidget(self.label_5) + self.xOffsetEdit = QtWidgets.QLineEdit(self.moveActionTab) + self.xOffsetEdit.setObjectName("xOffsetEdit") + self.horizontalLayout_3.addWidget(self.xOffsetEdit) + self.label_4 = QtWidgets.QLabel(self.moveActionTab) + self.label_4.setMinimumSize(QtCore.QSize(70, 0)) + self.label_4.setObjectName("label_4") + self.horizontalLayout_3.addWidget(self.label_4) + self.label_6 = QtWidgets.QLabel(self.moveActionTab) + self.label_6.setMinimumSize(QtCore.QSize(25, 0)) + self.label_6.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.label_6.setObjectName("label_6") + self.horizontalLayout_3.addWidget(self.label_6) + self.yOffsetEdit = QtWidgets.QLineEdit(self.moveActionTab) + self.yOffsetEdit.setObjectName("yOffsetEdit") + self.horizontalLayout_3.addWidget(self.yOffsetEdit) + self.label_7 = QtWidgets.QLabel(self.moveActionTab) + self.label_7.setMinimumSize(QtCore.QSize(70, 0)) + self.label_7.setObjectName("label_7") + self.horizontalLayout_3.addWidget(self.label_7) + self.gridLayout_3.addLayout(self.horizontalLayout_3, 2, 0, 1, 6) + self.horizontalLayout_4 = QtWidgets.QHBoxLayout() + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.scrollUp = QtWidgets.QRadioButton(self.moveActionTab) + self.scrollUp.setMinimumSize(QtCore.QSize(100, 0)) + self.scrollUp.setAutoExclusive(True) + self.scrollUp.setObjectName("scrollUp") + self.horizontalLayout_4.addWidget(self.scrollUp) + self.scrollUpEdit = QtWidgets.QLineEdit(self.moveActionTab) + self.scrollUpEdit.setObjectName("scrollUpEdit") + self.horizontalLayout_4.addWidget(self.scrollUpEdit) + self.label_8 = QtWidgets.QLabel(self.moveActionTab) + self.label_8.setMinimumSize(QtCore.QSize(80, 0)) + self.label_8.setObjectName("label_8") + self.horizontalLayout_4.addWidget(self.label_8) + self.scrollDown = QtWidgets.QRadioButton(self.moveActionTab) + self.scrollDown.setMinimumSize(QtCore.QSize(110, 0)) + self.scrollDown.setAutoExclusive(True) + self.scrollDown.setObjectName("scrollDown") + self.horizontalLayout_4.addWidget(self.scrollDown) + self.scrollDownEdit = QtWidgets.QLineEdit(self.moveActionTab) + self.scrollDownEdit.setObjectName("scrollDownEdit") + self.horizontalLayout_4.addWidget(self.scrollDownEdit) + self.label_9 = QtWidgets.QLabel(self.moveActionTab) + self.label_9.setMinimumSize(QtCore.QSize(80, 0)) + self.label_9.setObjectName("label_9") + self.horizontalLayout_4.addWidget(self.label_9) + self.gridLayout_3.addLayout(self.horizontalLayout_4, 3, 0, 1, 1) + self.mouseActionTabWidget.addTab(self.moveActionTab, "") + self.gridLayout.addWidget(self.mouseActionTabWidget, 0, 0, 1, 1) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setSpacing(20) + self.horizontalLayout.setObjectName("horizontalLayout") + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout.addItem(spacerItem) + self.ok = QtWidgets.QPushButton(MouseActionEditDialog) + self.ok.setMinimumSize(QtCore.QSize(130, 0)) + self.ok.setAutoDefault(False) + self.ok.setObjectName("ok") + self.horizontalLayout.addWidget(self.ok) + self.cancel = QtWidgets.QPushButton(MouseActionEditDialog) + self.cancel.setMinimumSize(QtCore.QSize(130, 0)) + self.cancel.setAutoDefault(False) + self.cancel.setObjectName("cancel") + self.horizontalLayout.addWidget(self.cancel) + self.gridLayout.addLayout(self.horizontalLayout, 3, 0, 1, 1) + + self.retranslateUi(MouseActionEditDialog) + self.mouseActionTabWidget.setCurrentIndex(0) + QtCore.QMetaObject.connectSlotsByName(MouseActionEditDialog) + + def retranslateUi(self, MouseActionEditDialog): + _translate = QtCore.QCoreApplication.translate + MouseActionEditDialog.setWindowTitle(_translate("MouseActionEditDialog", "Dialog")) + self.leftClick.setText(_translate("MouseActionEditDialog", "Click left button")) + self.rightClick.setText(_translate("MouseActionEditDialog", "Click right button")) + self.middleClick.setText(_translate("MouseActionEditDialog", "Click middle button")) + self.leftDclick.setText(_translate("MouseActionEditDialog", "Double-click left button")) + self.rightDclick.setText(_translate("MouseActionEditDialog", "Double-click right button")) + self.middleDclick.setText(_translate("MouseActionEditDialog", "Double-click middle button")) + self.leftDown.setText(_translate("MouseActionEditDialog", "left button down")) + self.rightDown.setText(_translate("MouseActionEditDialog", "right button down")) + self.middleDown.setText(_translate("MouseActionEditDialog", "middle button down")) + self.leftUp.setText(_translate("MouseActionEditDialog", "left button up")) + self.rightUp.setText(_translate("MouseActionEditDialog", "right button up")) + self.middleUp.setText(_translate("MouseActionEditDialog", "middle button up")) + self.mouseActionTabWidget.setTabText(self.mouseActionTabWidget.indexOf(self.clickActionTab), _translate("MouseActionEditDialog", "Click")) + self.moveTo.setText(_translate("MouseActionEditDialog", "Move to:")) + self.label_2.setText(_translate("MouseActionEditDialog", "X")) + self.label_3.setText(_translate("MouseActionEditDialog", "Y")) + self.moveOffset.setText(_translate("MouseActionEditDialog", "Move offset")) + self.label_5.setText(_translate("MouseActionEditDialog", "X")) + self.label_4.setText(_translate("MouseActionEditDialog", "pixels")) + self.label_6.setText(_translate("MouseActionEditDialog", "Y")) + self.label_7.setText(_translate("MouseActionEditDialog", "pixels")) + self.scrollUp.setText(_translate("MouseActionEditDialog", "Scroll up")) + self.label_8.setText(_translate("MouseActionEditDialog", "pixels")) + self.scrollDown.setText(_translate("MouseActionEditDialog", "Scroll down")) + self.label_9.setText(_translate("MouseActionEditDialog", "pixels")) + self.mouseActionTabWidget.setTabText(self.mouseActionTabWidget.indexOf(self.moveActionTab), _translate("MouseActionEditDialog", "Move")) + self.ok.setText(_translate("MouseActionEditDialog", "OK")) + self.cancel.setText(_translate("MouseActionEditDialog", "Cancel")) + + + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + MouseActionEditDialog = QtWidgets.QDialog() + ui = Ui_MouseActionEditDialog() + ui.setupUi(MouseActionEditDialog) + MouseActionEditDialog.show() + sys.exit(app.exec_()) diff --git a/ui_pauseactioneditwnd.py b/ui_pauseactioneditwnd.py new file mode 100644 index 0000000..a793e89 --- /dev/null +++ b/ui_pauseactioneditwnd.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'pauseactioneditwnd.ui' +# +# Created by: PyQt5 UI code generator 5.12.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_PauseActionEditDialog(object): + def setupUi(self, PauseActionEditDialog): + PauseActionEditDialog.setObjectName("PauseActionEditDialog") + PauseActionEditDialog.resize(271, 133) + font = QtGui.QFont() + font.setPointSize(10) + PauseActionEditDialog.setFont(font) + self.gridLayout = QtWidgets.QGridLayout(PauseActionEditDialog) + self.gridLayout.setObjectName("gridLayout") + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.label = QtWidgets.QLabel(PauseActionEditDialog) + self.label.setObjectName("label") + self.horizontalLayout.addWidget(self.label) + self.secondEdit = QtWidgets.QLineEdit(PauseActionEditDialog) + self.secondEdit.setObjectName("secondEdit") + self.horizontalLayout.addWidget(self.secondEdit) + self.label_2 = QtWidgets.QLabel(PauseActionEditDialog) + self.label_2.setObjectName("label_2") + self.horizontalLayout.addWidget(self.label_2) + self.gridLayout.addLayout(self.horizontalLayout, 0, 0, 1, 1) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setSpacing(20) + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_2.addItem(spacerItem) + self.ok = QtWidgets.QPushButton(PauseActionEditDialog) + self.ok.setMinimumSize(QtCore.QSize(100, 0)) + self.ok.setObjectName("ok") + self.horizontalLayout_2.addWidget(self.ok) + self.cancel = QtWidgets.QPushButton(PauseActionEditDialog) + self.cancel.setMinimumSize(QtCore.QSize(100, 0)) + self.cancel.setObjectName("cancel") + self.horizontalLayout_2.addWidget(self.cancel) + self.gridLayout.addLayout(self.horizontalLayout_2, 1, 0, 1, 1) + + self.retranslateUi(PauseActionEditDialog) + QtCore.QMetaObject.connectSlotsByName(PauseActionEditDialog) + + def retranslateUi(self, PauseActionEditDialog): + _translate = QtCore.QCoreApplication.translate + PauseActionEditDialog.setWindowTitle(_translate("PauseActionEditDialog", "Pause Action Edit Dialog")) + self.label.setText(_translate("PauseActionEditDialog", "Pause for ")) + self.label_2.setText(_translate("PauseActionEditDialog", "seconds")) + self.ok.setText(_translate("PauseActionEditDialog", "OK")) + self.cancel.setText(_translate("PauseActionEditDialog", "Cancel")) + + + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + PauseActionEditDialog = QtWidgets.QDialog() + ui = Ui_PauseActionEditDialog() + ui.setupUi(PauseActionEditDialog) + PauseActionEditDialog.show() + sys.exit(app.exec_()) diff --git a/ui_profileeditwnd.py b/ui_profileeditwnd.py new file mode 100644 index 0000000..270dc0c --- /dev/null +++ b/ui_profileeditwnd.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'profileeditwnd.ui' +# +# Created by: PyQt5 UI code generator 5.12.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_ProfileEditDialog(object): + def setupUi(self, ProfileEditDialog): + ProfileEditDialog.setObjectName("ProfileEditDialog") + ProfileEditDialog.resize(1191, 591) + self.gridLayout_2 = QtWidgets.QGridLayout(ProfileEditDialog) + self.gridLayout_2.setObjectName("gridLayout_2") + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.label = QtWidgets.QLabel(ProfileEditDialog) + self.label.setMinimumSize(QtCore.QSize(60, 0)) + self.label.setObjectName("label") + self.horizontalLayout.addWidget(self.label) + self.profileNameEdit = QtWidgets.QLineEdit(ProfileEditDialog) + self.profileNameEdit.setObjectName("profileNameEdit") + self.horizontalLayout.addWidget(self.profileNameEdit) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout.addItem(spacerItem) + self.gridLayout_2.addLayout(self.horizontalLayout, 0, 0, 1, 1) + self.groupBox = QtWidgets.QGroupBox(ProfileEditDialog) + self.groupBox.setObjectName("groupBox") + self.gridLayout = QtWidgets.QGridLayout(self.groupBox) + self.gridLayout.setObjectName("gridLayout") + self.cmdTable = QtWidgets.QTableWidget(self.groupBox) + self.cmdTable.setMinimumSize(QtCore.QSize(0, 200)) + font = QtGui.QFont() + font.setPointSize(11) + self.cmdTable.setFont(font) + self.cmdTable.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + self.cmdTable.setDragDropOverwriteMode(False) + self.cmdTable.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) + self.cmdTable.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) + self.cmdTable.setColumnCount(2) + self.cmdTable.setObjectName("cmdTable") + self.cmdTable.setRowCount(0) + self.cmdTable.horizontalHeader().setDefaultSectionSize(160) + self.cmdTable.horizontalHeader().setMinimumSectionSize(160) + self.gridLayout.addWidget(self.cmdTable, 0, 0, 1, 1) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setSpacing(20) + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_2.addItem(spacerItem1) + spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_2.addItem(spacerItem2) + self.newCmd = QtWidgets.QPushButton(self.groupBox) + self.newCmd.setMinimumSize(QtCore.QSize(130, 0)) + self.newCmd.setObjectName("newCmd") + self.horizontalLayout_2.addWidget(self.newCmd) + self.editCmd = QtWidgets.QPushButton(self.groupBox) + self.editCmd.setMinimumSize(QtCore.QSize(130, 0)) + self.editCmd.setObjectName("editCmd") + self.horizontalLayout_2.addWidget(self.editCmd) + self.deleteCmd = QtWidgets.QPushButton(self.groupBox) + self.deleteCmd.setMinimumSize(QtCore.QSize(130, 0)) + self.deleteCmd.setObjectName("deleteCmd") + self.horizontalLayout_2.addWidget(self.deleteCmd) + self.gridLayout.addLayout(self.horizontalLayout_2, 1, 0, 1, 1) + self.gridLayout_2.addWidget(self.groupBox, 1, 0, 1, 1) + self.horizontalLayout_3 = QtWidgets.QHBoxLayout() + self.horizontalLayout_3.setSpacing(20) + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_3.addItem(spacerItem3) + self.ok = QtWidgets.QPushButton(ProfileEditDialog) + self.ok.setMinimumSize(QtCore.QSize(130, 0)) + self.ok.setObjectName("ok") + self.horizontalLayout_3.addWidget(self.ok) + self.cancel = QtWidgets.QPushButton(ProfileEditDialog) + self.cancel.setMinimumSize(QtCore.QSize(130, 0)) + self.cancel.setObjectName("cancel") + self.horizontalLayout_3.addWidget(self.cancel) + self.gridLayout_2.addLayout(self.horizontalLayout_3, 2, 0, 1, 1) + + self.retranslateUi(ProfileEditDialog) + QtCore.QMetaObject.connectSlotsByName(ProfileEditDialog) + + def retranslateUi(self, ProfileEditDialog): + _translate = QtCore.QCoreApplication.translate + ProfileEditDialog.setWindowTitle(_translate("ProfileEditDialog", "Dialog")) + self.label.setText(_translate("ProfileEditDialog", "Profile:")) + self.groupBox.setTitle(_translate("ProfileEditDialog", "Commands")) + self.newCmd.setText(_translate("ProfileEditDialog", "New")) + self.editCmd.setText(_translate("ProfileEditDialog", "Edit")) + self.deleteCmd.setText(_translate("ProfileEditDialog", "Delete")) + self.ok.setText(_translate("ProfileEditDialog", "OK")) + self.cancel.setText(_translate("ProfileEditDialog", "Cancel")) + + + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + ProfileEditDialog = QtWidgets.QDialog() + ui = Ui_ProfileEditDialog() + ui.setupUi(ProfileEditDialog) + ProfileEditDialog.show() + sys.exit(app.exec_())