|
from functools import wraps |
|
import re |
|
import errno |
|
import os |
|
import string |
|
import select |
|
import threading |
|
import time |
|
import enum |
|
|
|
from Xlib import X, XK, display |
|
from Xlib.ext import xinput, xtest |
|
from Xlib.ext.ge import GenericEventCode |
|
|
|
# Enable support for media keys. |
|
XK.load_keysym_group('xf86') |
|
# Load non-us keyboard related keysyms. |
|
XK.load_keysym_group('xkb') |
|
|
|
_SPLIT_RX = re.compile(r'(\s+|(?:\w+(?:\s*\()?)|.)') |
|
def parse_key_combo(combo_string, key_name_to_key_code=None): |
|
|
|
if key_name_to_key_code is None: |
|
key_name_to_key_code = lambda key_name: key_name |
|
|
|
key_events = [] |
|
down_keys = [] |
|
token = None |
|
count = 0 |
|
|
|
def _raise_error(exception, details): |
|
msg = '%s in "%s"' % ( |
|
details, |
|
combo_string[:count] + |
|
'[' + token + ']' + |
|
combo_string[count+len(token):], |
|
) |
|
raise exception(msg) |
|
|
|
for token in _SPLIT_RX.split(combo_string): |
|
if not token: |
|
continue |
|
|
|
if token.isspace(): |
|
pass |
|
|
|
elif re.match(r'\w', token): |
|
|
|
if token.endswith('('): |
|
key_name = token[:-1].rstrip().lower() |
|
release = False |
|
else: |
|
key_name = token.lower() |
|
release = True |
|
|
|
key_code = key_name_to_key_code(key_name) |
|
if key_code is None: |
|
_raise_error(ValueError, 'unknown key') |
|
elif key_code in down_keys: |
|
_raise_error(ValueError, 'key "%s" already pressed' % key_name) |
|
|
|
key_events.append((key_code, True)) |
|
|
|
if release: |
|
key_events.append((key_code, False)) |
|
else: |
|
down_keys.append(key_code) |
|
|
|
elif token == ')': |
|
if not down_keys: |
|
_raise_error(SyntaxError, 'unbalanced ")"') |
|
key_code = down_keys.pop() |
|
key_events.append((key_code, False)) |
|
|
|
else: |
|
_raise_error(SyntaxError, 'invalid character "%s"' % token) |
|
|
|
count += len(token) |
|
|
|
if down_keys: |
|
_raise_error(SyntaxError, 'unbalanced "("') |
|
|
|
return key_events |
|
|
|
# Create case insensitive mapping of keyname to keysym. |
|
KEY_TO_KEYSYM = {} |
|
for symbol in sorted(dir(XK)): # Sorted so XK_a is preferred over XK_A. |
|
if not symbol.startswith('XK_'): |
|
continue |
|
name = symbol[3:].lower() |
|
keysym = getattr(XK, symbol) |
|
KEY_TO_KEYSYM[name] = keysym |
|
# Add aliases for `XF86_` keys. |
|
if name.startswith('xf86_'): |
|
alias = name[5:] |
|
if alias not in KEY_TO_KEYSYM: |
|
KEY_TO_KEYSYM[alias] = keysym |
|
|
|
XINPUT_DEVICE_ID = xinput.AllDevices |
|
XINPUT_EVENT_MASK = xinput.KeyPressMask | xinput.KeyReleaseMask |
|
|
|
KEYCODE_TO_KEY = { |
|
# Function row. |
|
67: "F1", |
|
68: "F2", |
|
69: "F3", |
|
70: "F4", |
|
71: "F5", |
|
72: "F6", |
|
73: "F7", |
|
74: "F8", |
|
75: "F9", |
|
76: "F10", |
|
95: "F11", |
|
96: "F12", |
|
# Number row. |
|
49: "`", |
|
10: "1", |
|
11: "2", |
|
12: "3", |
|
13: "4", |
|
14: "5", |
|
15: "6", |
|
16: "7", |
|
17: "8", |
|
18: "9", |
|
19: "0", |
|
20: "-", |
|
21: "=", |
|
51: "\\", |
|
# Upper row. |
|
24: "q", |
|
25: "w", |
|
26: "e", |
|
27: "r", |
|
28: "t", |
|
29: "y", |
|
30: "u", |
|
31: "i", |
|
32: "o", |
|
33: "p", |
|
34: "[", |
|
35: "]", |
|
# Home row. |
|
38: "a", |
|
39: "s", |
|
40: "d", |
|
41: "f", |
|
42: "g", |
|
43: "h", |
|
44: "j", |
|
45: "k", |
|
46: "l", |
|
47: ";", |
|
48: "'", |
|
# Bottom row. |
|
52: "z", |
|
53: "x", |
|
54: "c", |
|
55: "v", |
|
56: "b", |
|
57: "n", |
|
58: "m", |
|
59: ",", |
|
60: ".", |
|
61: "/", |
|
# Other keys. |
|
22 : "BackSpace", |
|
119: "Delete", |
|
116: "Down", |
|
115: "End", |
|
9 : "Escape", |
|
110: "Home", |
|
118: "Insert", |
|
113: "Left", |
|
117: "Page_Down", |
|
112: "Page_Up", |
|
36 : "Return", |
|
114: "Right", |
|
23 : "Tab", |
|
111: "Up", |
|
65 : "space", |
|
} |
|
|
|
KEY_TO_KEYCODE = dict(zip(KEYCODE_TO_KEY.values(), KEYCODE_TO_KEY.keys())) |
|
|
|
def with_display_lock(func): |
|
""" |
|
Use this function as a decorator on a method of the XEventLoop class (or |
|
one of its subclasses) to acquire the _display_lock of the object before |
|
calling the function and release it afterwards. |
|
""" |
|
# To keep __doc__/__name__ attributes of the initial function. |
|
@wraps(func) |
|
def wrapped(self, *args, **kwargs): |
|
with self._display_lock: |
|
return func(self, *args, **kwargs) |
|
return wrapped |
|
|
|
class XEventLoop(threading.Thread): |
|
|
|
def __init__(self, name='xev'): |
|
super().__init__() |
|
self.name += '-' + name |
|
self._display = display.Display() |
|
self._pipe = os.pipe() |
|
self._display_lock = threading.Lock() |
|
self._readfds = (self._pipe[0], self._display.fileno()) |
|
|
|
def _on_event(self, event): |
|
pass |
|
|
|
@with_display_lock |
|
def _process_pending_events(self): |
|
for __ in range(self._display.pending_events()): |
|
self._on_event(self._display.next_event()) |
|
|
|
def run(self): |
|
while True: |
|
self._process_pending_events() |
|
# No more events: sleep until we get new data on the |
|
# display connection, or on the pipe used to signal |
|
# the end of the loop. |
|
try: |
|
rlist, wlist, xlist = select.select(self._readfds, (), ()) |
|
except select.error as err: |
|
if isinstance(err, OSError): |
|
code = err.errno |
|
else: |
|
code = err[0] |
|
if code != errno.EINTR: |
|
raise |
|
continue |
|
assert not wlist |
|
assert not xlist |
|
if self._pipe[0] in rlist: |
|
break |
|
# If we're here, rlist should contains the display fd, |
|
# and the next iteration will find some pending events. |
|
|
|
def cancel(self): |
|
# Wake up the capture thread... |
|
os.write(self._pipe[1], b'quit') |
|
# ...and wait for it to terminate. |
|
self.join() |
|
for fd in self._pipe: |
|
os.close(fd) |
|
|
|
|
|
class KeyboardCapture(XEventLoop): |
|
"""Listen to keyboard press and release events.""" |
|
|
|
SUPPORTED_KEYS_LAYOUT = ''' |
|
Escape F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12 |
|
` 1 2 3 4 5 6 7 8 9 0 - = \\ BackSpace Insert Home Page_Up |
|
Tab q w e r t y u i o p [ ] Delete End Page_Down |
|
a s d f g h j k l ; ' Return |
|
z x c v b n m , . / Up |
|
space Left Down Right |
|
''' |
|
SUPPORTED_KEYS = tuple(SUPPORTED_KEYS_LAYOUT.split()) |
|
|
|
def __init__(self): |
|
"""Prepare to listen for keyboard events.""" |
|
super().__init__(name='capture') |
|
self._window = self._display.screen().root |
|
if not self._display.has_extension('XInputExtension'): |
|
raise Exception('Xlib\'s XInput extension is required, but could not be found.') |
|
self._suppressed_keys = set() |
|
self._devices = [] |
|
self.key_down = lambda key: None |
|
self.key_up = lambda key: None |
|
|
|
def _update_devices(self): |
|
# Find all keyboard devices. |
|
# This function is never called while the event loop thread is running, |
|
# so it is unnecessary to lock self._display_lock. |
|
keyboard_devices = [] |
|
for devinfo in self._display.xinput_query_device(xinput.AllDevices).devices: |
|
# Only keep slave devices. |
|
# Note: we look at pointer devices too, as some keyboards (like the |
|
# VicTop mechanical gaming keyboard) register 2 devices, including |
|
# a pointer device with a key class (to fully support NKRO). |
|
if devinfo.use not in (xinput.SlaveKeyboard, xinput.SlavePointer): |
|
continue |
|
# Ignore XTest keyboard device. |
|
if 'Virtual core XTEST keyboard' == devinfo.name: |
|
continue |
|
# Ignore disabled devices. |
|
if not devinfo.enabled: |
|
continue |
|
# Check for the presence of a key class. |
|
for c in devinfo.classes: |
|
if c.type == xinput.KeyClass: |
|
keyboard_devices.append(devinfo.deviceid) |
|
break |
|
if XINPUT_DEVICE_ID == xinput.AllDevices: |
|
self._devices = keyboard_devices |
|
else: |
|
self._devices = [XINPUT_DEVICE_ID] |
|
|
|
def _on_event(self, event): |
|
if event.type != GenericEventCode: |
|
return |
|
if event.evtype not in (xinput.KeyPress, xinput.KeyRelease): |
|
return |
|
assert event.data.sourceid in self._devices |
|
keycode = event.data.detail |
|
modifiers = event.data.mods.effective_mods & ~0b10000 & 0xFF |
|
key = KEYCODE_TO_KEY.get(keycode) |
|
if key is None: |
|
# Not a supported key, ignore... |
|
return |
|
# ...or pass it on to a callback method. |
|
if event.evtype == xinput.KeyPress: |
|
# Ignore event if a modifier is set. |
|
if modifiers == 0: |
|
self.key_down(key) |
|
elif event.evtype == xinput.KeyRelease: |
|
self.key_up(key) |
|
|
|
def start(self): |
|
self._update_devices() |
|
self._window.xinput_select_events([ |
|
(deviceid, XINPUT_EVENT_MASK) |
|
for deviceid in self._devices |
|
]) |
|
suppressed_keys = self._suppressed_keys |
|
self._suppressed_keys = set() |
|
self.suppress_keyboard(suppressed_keys) |
|
super().start() |
|
|
|
def cancel(self): |
|
self.suppress_keyboard() |
|
super().cancel() |
|
|
|
def _grab_key(self, keycode): |
|
for deviceid in self._devices: |
|
self._window.xinput_grab_keycode(deviceid, |
|
X.CurrentTime, |
|
keycode, |
|
xinput.GrabModeAsync, |
|
xinput.GrabModeAsync, |
|
True, |
|
XINPUT_EVENT_MASK, |
|
(0, X.Mod2Mask)) |
|
|
|
def _ungrab_key(self, keycode): |
|
for deviceid in self._devices: |
|
self._window.xinput_ungrab_keycode(deviceid, |
|
keycode, |
|
(0, X.Mod2Mask)) |
|
|
|
@with_display_lock |
|
def suppress_keyboard(self, suppressed_keys=()): |
|
suppressed_keys = set(suppressed_keys) |
|
if self._suppressed_keys == suppressed_keys: |
|
return |
|
for key in self._suppressed_keys - suppressed_keys: |
|
self._ungrab_key(KEY_TO_KEYCODE[key]) |
|
self._suppressed_keys.remove(key) |
|
for key in suppressed_keys - self._suppressed_keys: |
|
self._grab_key(KEY_TO_KEYCODE[key]) |
|
self._suppressed_keys.add(key) |
|
assert self._suppressed_keys == suppressed_keys |
|
self._display.sync() |
|
|
|
def is_latin1(code): |
|
return 0x20 <= code <= 0x7e or 0xa0 <= code <= 0xff |
|
|
|
def uchr_to_keysym(char): |
|
code = ord(char) |
|
# Latin-1 characters: direct, 1:1 mapping. |
|
if is_latin1(code): |
|
return code |
|
if 0x09 == code: |
|
return XK.XK_Tab |
|
if 0x0a == code or 0x0d == code: |
|
return XK.XK_Return |
|
return UCS_TO_KEYSYM.get(code, code | 0x01000000) |
|
|
|
def keysym_to_string(keysym): |
|
# Latin-1 characters: direct, 1:1 mapping. |
|
if is_latin1(keysym): |
|
code = keysym |
|
elif (keysym & 0xff000000) == 0x01000000: |
|
code = keysym & 0x00ffffff |
|
else: |
|
code = KEYSYM_TO_UCS.get(keysym) |
|
if code is None: |
|
keysym_str = XK.keysym_to_string(keysym) |
|
if keysym_str is None: |
|
keysym_str = '' |
|
for c in keysym_str: |
|
if c not in string.printable: |
|
keysym_str = '' |
|
break |
|
return keysym_str |
|
return chr(code) |
|
|
|
|
|
class KeyboardEmulation: |
|
"""Emulate keyboard events.""" |
|
|
|
class Mapping: |
|
|
|
def __init__(self, keycode, modifiers, keysym, custom_mapping=None): |
|
self.keycode = keycode |
|
self.modifiers = modifiers |
|
self.keysym = keysym |
|
self.custom_mapping = custom_mapping |
|
|
|
def __str__(self): |
|
return '%u:%x=%x[%s]%s' % ( |
|
self.keycode, self.modifiers, |
|
self.keysym, keysym_to_string(self.keysym), |
|
'' if self.custom_mapping is None else '*', |
|
) |
|
|
|
# We can use the first 2 entry of a X11 mapping: |
|
# keycode and keycode+shift. The 3rd entry is a |
|
# special keysym to mark the mapping. |
|
CUSTOM_MAPPING_LENGTH = 3 |
|
# Special keysym to mark custom keyboard mappings. |
|
PLOVER_MAPPING_KEYSYM = 0x01ffffff |
|
# Free unused keysym. |
|
UNUSED_KEYSYM = 0xffffff # XK_VoidSymbol |
|
|
|
def __init__(self): |
|
"""Prepare to emulate keyboard events.""" |
|
self._display = display.Display() |
|
self._update_keymap() |
|
|
|
def _update_keymap(self): |
|
'''Analyse keymap, build a mapping of keysym to (keycode + modifiers), |
|
and find unused keycodes that can be used for unmapped keysyms. |
|
''' |
|
self._keymap = {} |
|
self._custom_mappings_queue = [] |
|
# Analyse X11 keymap. |
|
keycode = self._display.display.info.min_keycode |
|
keycode_count = self._display.display.info.max_keycode - keycode + 1 |
|
for mapping in self._display.get_keyboard_mapping(keycode, keycode_count): |
|
mapping = tuple(mapping) |
|
while mapping and X.NoSymbol == mapping[-1]: |
|
mapping = mapping[:-1] |
|
if not mapping: |
|
# Free never used before keycode. |
|
custom_mapping = [self.UNUSED_KEYSYM] * self.CUSTOM_MAPPING_LENGTH |
|
custom_mapping[-1] = self.PLOVER_MAPPING_KEYSYM |
|
mapping = custom_mapping |
|
elif self.CUSTOM_MAPPING_LENGTH == len(mapping) and \ |
|
self.PLOVER_MAPPING_KEYSYM == mapping[-1]: |
|
# Keycode was previously used by Plover. |
|
custom_mapping = list(mapping) |
|
else: |
|
# Used keycode. |
|
custom_mapping = None |
|
for keysym_index, keysym in enumerate(mapping): |
|
if keysym == self.PLOVER_MAPPING_KEYSYM: |
|
continue |
|
if keysym_index not in (0, 1, 4, 5): |
|
continue |
|
modifiers = 0 |
|
if 1 == (keysym_index % 2): |
|
# The keycode needs the Shift modifier. |
|
modifiers |= X.ShiftMask |
|
if 4 <= keysym_index <= 5: |
|
# 3rd (AltGr) level. |
|
modifiers |= X.Mod5Mask |
|
mapping = self.Mapping(keycode, modifiers, keysym, custom_mapping) |
|
if keysym != X.NoSymbol and keysym != self.UNUSED_KEYSYM: |
|
# Some keysym are mapped multiple times, prefer lower modifiers combos. |
|
previous_mapping = self._keymap.get(keysym) |
|
if previous_mapping is None or mapping.modifiers < previous_mapping.modifiers: |
|
self._keymap[keysym] = mapping |
|
if custom_mapping is not None: |
|
self._custom_mappings_queue.append(mapping) |
|
keycode += 1 |
|
# Determine the backspace mapping. |
|
backspace_keysym = XK.string_to_keysym('BackSpace') |
|
self._backspace_mapping = self._get_mapping(backspace_keysym) |
|
assert self._backspace_mapping is not None |
|
assert self._backspace_mapping.custom_mapping is None |
|
# Get modifier mapping. |
|
self.modifier_mapping = self._display.get_modifier_mapping() |
|
|
|
def send_backspaces(self, number_of_backspaces): |
|
"""Emulate the given number of backspaces. |
|
|
|
The emulated backspaces are not detected by KeyboardCapture. |
|
|
|
Argument: |
|
|
|
number_of_backspace -- The number of backspaces to emulate. |
|
|
|
""" |
|
for x in range(number_of_backspaces): |
|
self._send_keycode(self._backspace_mapping.keycode, |
|
self._backspace_mapping.modifiers) |
|
self._display.sync() |
|
|
|
def send_string(self, s): |
|
"""Emulate the given string. |
|
|
|
The emulated string is not detected by KeyboardCapture. |
|
|
|
Argument: |
|
|
|
s -- The string to emulate. |
|
|
|
""" |
|
for char in s: |
|
keysym = uchr_to_keysym(char) |
|
mapping = self._get_mapping(keysym) |
|
if mapping is None: |
|
continue |
|
self._send_keycode(mapping.keycode, |
|
mapping.modifiers) |
|
self._display.sync() |
|
|
|
def send_key_combination(self, combo_string): |
|
"""Emulate a sequence of key combinations. |
|
|
|
KeyboardCapture instance would normally detect the emulated |
|
key events. In order to prevent this, all KeyboardCapture |
|
instances are told to ignore the emulated key events. |
|
|
|
Argument: |
|
|
|
combo_string -- A string representing a sequence of key |
|
combinations. Keys are represented by their names in the |
|
Xlib.XK module, without the 'XK_' prefix. For example, the |
|
left Alt key is represented by 'Alt_L'. Keys are either |
|
separated by a space or a left or right parenthesis. |
|
Parentheses must be properly formed in pairs and may be |
|
nested. A key immediately followed by a parenthetical |
|
indicates that the key is pressed down while all keys enclosed |
|
in the parenthetical are pressed and released in turn. For |
|
example, Alt_L(Tab) means to hold the left Alt key down, press |
|
and release the Tab key, and then release the left Alt key. |
|
|
|
""" |
|
# Parse and validate combo. |
|
key_events = [ |
|
(keycode, X.KeyPress if pressed else X.KeyRelease) for keycode, pressed |
|
in parse_key_combo(combo_string, self._get_keycode_from_keystring) |
|
] |
|
# Emulate the key combination by sending key events. |
|
for keycode, event_type in key_events: |
|
xtest.fake_input(self._display, event_type, keycode) |
|
self._display.sync() |
|
|
|
def _send_keycode(self, keycode, modifiers=0): |
|
"""Emulate a key press and release. |
|
|
|
Arguments: |
|
|
|
keycode -- An integer in the inclusive range [8-255]. |
|
|
|
modifiers -- An 8-bit bit mask indicating if the key |
|
pressed is modified by other keys, such as Shift, Capslock, |
|
Control, and Alt. |
|
|
|
""" |
|
modifiers_list = [ |
|
self.modifier_mapping[n][0] |
|
for n in range(8) |
|
if (modifiers & (1 << n)) |
|
] |
|
# Press modifiers. |
|
for mod_keycode in modifiers_list: |
|
xtest.fake_input(self._display, X.KeyPress, mod_keycode) |
|
# Press and release the base key. |
|
xtest.fake_input(self._display, X.KeyPress, keycode) |
|
xtest.fake_input(self._display, X.KeyRelease, keycode) |
|
# Release modifiers. |
|
for mod_keycode in reversed(modifiers_list): |
|
xtest.fake_input(self._display, X.KeyRelease, mod_keycode) |
|
|
|
def _get_keycode_from_keystring(self, keystring): |
|
'''Find the physical key <keystring> is mapped to. |
|
|
|
Return None of if keystring is not mapped. |
|
''' |
|
keysym = KEY_TO_KEYSYM.get(keystring) |
|
if keysym is None: |
|
return None |
|
mapping = self._get_mapping(keysym, automatically_map=False) |
|
if mapping is None: |
|
return None |
|
return mapping.keycode |
|
|
|
def _get_mapping(self, keysym, automatically_map=True): |
|
"""Return a keycode and modifier mask pair that result in the keysym. |
|
|
|
There is a one-to-many mapping from keysyms to keycode and |
|
modifiers pairs; this function returns one of the possibly |
|
many valid mappings, or None if no mapping exists, and a |
|
new one cannot be added. |
|
|
|
Arguments: |
|
|
|
keysym -- A key symbol. |
|
|
|
""" |
|
mapping = self._keymap.get(keysym) |
|
if mapping is None: |
|
# Automatically map? |
|
if not automatically_map: |
|
# No. |
|
return None |
|
# Can we map it? |
|
if 0 == len(self._custom_mappings_queue): |
|
# Nope... |
|
return None |
|
mapping = self._custom_mappings_queue.pop(0) |
|
previous_keysym = mapping.keysym |
|
keysym_index = mapping.custom_mapping.index(previous_keysym) |
|
# Update X11 keymap. |
|
mapping.custom_mapping[keysym_index] = keysym |
|
self._display.change_keyboard_mapping(mapping.keycode, [mapping.custom_mapping]) |
|
# Update our keymap. |
|
if previous_keysym in self._keymap: |
|
del self._keymap[previous_keysym] |
|
mapping.keysym = keysym |
|
self._keymap[keysym] = mapping |
|
# Move custom mapping back at the end of |
|
# the queue so we don't use it too soon. |
|
self._custom_mappings_queue.append(mapping) |
|
elif mapping.custom_mapping is not None: |
|
# Same as above; prevent mapping |
|
# from being reused to soon. |
|
self._custom_mappings_queue.remove(mapping) |
|
self._custom_mappings_queue.append(mapping) |
|
return mapping |
|
|
|
taipo = {frozenset({'e', 'space'}): 'E', |
|
frozenset({'space', 't'}): 'T', |
|
frozenset({'space', 'o'}): 'O', |
|
frozenset({'space', 'a'}): 'A', |
|
frozenset({'e', 'BackSpace'}): '(', |
|
frozenset({'t', 'BackSpace'}): '[', |
|
frozenset({'o', 'BackSpace'}): '{', |
|
frozenset({'BackSpace', 'a'}): '<', |
|
frozenset({'space', 'i'}): 'I', |
|
frozenset({'space', 'n'}): 'N', |
|
frozenset({'space', 's'}): 'S', |
|
frozenset({'r', 'space'}): 'R', |
|
frozenset({'i', 'BackSpace'}): ')', |
|
frozenset({'n', 'BackSpace'}): ']', |
|
frozenset({'s', 'BackSpace'}): '}', |
|
frozenset({'r', 'BackSpace'}): '>', |
|
frozenset({'e', 't'}): 'h', |
|
frozenset({'o', 'a'}): 'l', |
|
frozenset({'n', 'i'}): 'y', |
|
frozenset({'r', 's'}): 'b', |
|
frozenset({'e', 'space', 't'}): 'H', |
|
frozenset({'space', 'o', 'a'}): 'L', |
|
frozenset({'n', 'space', 'i'}): 'Y', |
|
frozenset({'space', 'r', 's'}): 'B', |
|
frozenset({'e', 't', 'BackSpace'}): '0', |
|
frozenset({'BackSpace', 'o', 'a'}): '4', |
|
frozenset({'n', 'i', 'BackSpace'}): '5', |
|
frozenset({'r', 's', 'BackSpace'}): '9', |
|
frozenset({'r', 'i'}): 'g', |
|
frozenset({'o', 't'}): 'u', |
|
frozenset({'e', 'a'}): 'c', |
|
frozenset({'n', 's'}): 'p', |
|
frozenset({'space', 'r', 'i'}): 'G', |
|
frozenset({'o', 'space', 't'}): 'U', |
|
frozenset({'e', 'space', 'a'}): 'C', |
|
frozenset({'space', 'n', 's'}): 'P', |
|
frozenset({'r', 'i', 'BackSpace'}): '#', |
|
frozenset({'o', 't', 'BackSpace'}): '2', |
|
frozenset({'e', 'BackSpace', 'a'}): '@', |
|
frozenset({'n', 'BackSpace', 's'}): '7', |
|
frozenset({'e', 'o'}): 'd', |
|
frozenset({'t', 'a'}): 'q', |
|
frozenset({'i', 's'}): 'f', |
|
frozenset({'r', 'n'}): 'z', |
|
frozenset({'e', 'space', 'o'}): 'D', |
|
frozenset({'space', 't', 'a'}): 'Q', |
|
frozenset({'space', 'i', 's'}): 'F', |
|
frozenset({'space', 'r', 'n'}): 'Z', |
|
frozenset({'e', 'o', 'BackSpace'}): '1', |
|
frozenset({'BackSpace', 't', 'a'}): '3', |
|
frozenset({'i', 'BackSpace', 's'}): '6', |
|
frozenset({'r', 'n', 'BackSpace'}): '8', |
|
frozenset({'e', 's'}): 'm', |
|
frozenset({'r', 't'}): 'x', |
|
frozenset({'o', 'i'}): 'k', |
|
frozenset({'n', 'a'}): 'j', |
|
frozenset({'e', 'space', 's'}): 'M', |
|
frozenset({'space', 'r', 't'}): 'X', |
|
frozenset({'o', 'space', 'i'}): 'K', |
|
frozenset({'space', 'n', 'a'}): 'J', |
|
frozenset({'e', 's', 'BackSpace'}): '*', |
|
frozenset({'r', 't', 'BackSpace'}): '^', |
|
frozenset({'o', 'i', 'BackSpace'}): '+', |
|
frozenset({'BackSpace', 'n', 'a'}): '=', |
|
frozenset({'o', 'n'}): '-', |
|
frozenset({'i', 'a'}): 'w', |
|
frozenset({'e', 'r'}): 'v', |
|
frozenset({'t', 's'}): '/', |
|
frozenset({'o', 'space', 'n'}): '_', |
|
frozenset({'space', 'i', 'a'}): 'W', |
|
frozenset({'e', 'r', 'space'}): 'V', |
|
frozenset({'t', 'space', 's'}): '\\', |
|
frozenset({'o', 'n', 'BackSpace'}): '%', |
|
frozenset({'BackSpace', 'i', 'a'}): '&', |
|
frozenset({'e', 'r', 'BackSpace'}): '$', |
|
frozenset({'t', 's', 'BackSpace'}): '|', |
|
frozenset({'s', 'a'}): "'", |
|
frozenset({'r', 'o'}): ';', |
|
frozenset({'t', 'i'}): '?', |
|
frozenset({'e', 'n'}): ',', |
|
frozenset({'space', 's', 'a'}): '"', |
|
frozenset({'space', 'r', 'o'}): ':', |
|
frozenset({'t', 'space', 'i'}): '!', |
|
frozenset({'e', 'space', 'n'}): '.', |
|
frozenset({'BackSpace', 's', 'a'}): '`', |
|
frozenset({'e', 'n', 'BackSpace'}): '~', |
|
frozenset({'e', 'i'}): 'Shift_L', |
|
frozenset({'t', 'n'}): 'Control_L', |
|
frozenset({'o', 's'}): 'Alt_L', |
|
frozenset({'a', 'r'}): 'Super_L', |
|
frozenset({'e', 't', 'o'}): 'Return', |
|
frozenset({'i', 'n', 's'}): 'Tab', |
|
frozenset({'t', 'o', 'a'}): 'Escape', |
|
|
|
frozenset({'e', 'n', 'o'}): 'Up', |
|
frozenset({'i', 't', 's'}): 'Down', |
|
frozenset({'i', 'n', 'o'}): 'Right', |
|
frozenset({'e', 'n', 's'}): 'Left', |
|
} |
|
|
|
|
|
capture = KeyboardCapture() |
|
capture.suppress_keyboard(KeyboardCapture.SUPPORTED_KEYS) |
|
capture.start() |
|
emulation = KeyboardEmulation() |
|
|
|
held = set() |
|
released = set() |
|
|
|
translation = {"q": "r", |
|
"w": "s", |
|
"e": "n", |
|
"r": "i", |
|
"u": "i", |
|
"i": "n", |
|
"o": "s", |
|
"p": "r", |
|
|
|
"a": "a", |
|
"s": "o", |
|
"d": "t", |
|
"f": "e", |
|
"j": "e", |
|
"k": "t", |
|
"l": "o", |
|
";": "a", |
|
|
|
"v": "space", |
|
"n": "space", |
|
|
|
"c": "BackSpace", |
|
"m": "BackSpace" |
|
} |
|
|
|
mod_queue = set() |
|
|
|
class Hand(enum.Enum): |
|
none = 0 |
|
left = 1 |
|
right = 2 |
|
last_hand = Hand.none |
|
|
|
def send(k): |
|
if k == "BackSpace": |
|
emulation.send_backspaces(1) |
|
elif len(mod_queue) > 0 or k in ["Tab", "Return", "Escape", "Left", "Down", "Right", "Up"]: |
|
s = "" |
|
for m in mod_queue: |
|
s += m + "(" |
|
s += k |
|
s += ")" * len(mod_queue) |
|
emulation.send_key_combination(s) |
|
mod_queue.clear() |
|
else: |
|
if k == "space": |
|
k = " " |
|
emulation.send_string(k) |
|
|
|
def hold(k): |
|
global last_hand |
|
hand = Hand.none |
|
if k in ['q', 'w', 'e', 'r', 'a', 's', 'd', 'f']: |
|
hand = Hand.left |
|
elif k in ['u', 'i', 'o', 'p', 'j', 'k', 'l', ';']: |
|
hand = Hand.right |
|
|
|
if last_hand != Hand.none and hand != Hand.none and last_hand != hand: |
|
for h in held: |
|
released.add(h) |
|
held.clear() |
|
combo() |
|
|
|
last_hand = hand |
|
|
|
if k in translation: |
|
k = translation[k] |
|
held.add(k) |
|
|
|
def combo(): |
|
new = set() |
|
for e in released: |
|
new.add(e[0]) |
|
fz = frozenset(new) |
|
if fz in taipo: |
|
t = taipo[fz] |
|
if t in ["Shift_L", "Control_L", "Alt_L", "Super_L"]: |
|
mod_queue.add(t) |
|
else: |
|
send(taipo[fz]) |
|
elif len(fz) == 1: |
|
for x in fz: |
|
send(x) |
|
|
|
global last_hand |
|
last_hand = Hand.none |
|
released.clear() |
|
|
|
def release(k): |
|
if k in translation: |
|
k = translation[k] |
|
if k in held: |
|
held.remove(k) |
|
|
|
now = time.time() |
|
for e in released.copy(): |
|
if now - e[1] > 0.18: |
|
released.remove(e) |
|
|
|
released.add((k, now)) |
|
|
|
if len(held) == 0: |
|
combo() |
|
|
|
capture.key_down = lambda k: hold(k) |
|
capture.key_up = lambda k: release(k) |