Skip to content

Instantly share code, notes, and snippets.

@semilin
Last active April 5, 2022 22:54
Show Gist options
  • Save semilin/fd2e7b7e98cb16cf84e3c321a43fd90a to your computer and use it in GitHub Desktop.
Save semilin/fd2e7b7e98cb16cf84e3c321a43fd90a to your computer and use it in GitHub Desktop.
Very poorly hacked together Taipo impl for Linux

Description

This is not a pure taipo implementation - I changed a couple keys around. Starting at line 638 is where the chords are defined, if you want to edit them.

Usage

The qwerty home row is used as the Taipo bottom row, and the one above that the top row. Qwerty C and M are backspace, and V and N are space. However, you can also just use the real spacebar if you prefer.

Credit

This code is almost entirely ripped from Plover's Linux keyboard code, so don't give much credit to me: original source code. All I wrote was the management of Taipo's chords at the bottom of the file.

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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment