_nixkeyboard.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. # -*- coding: utf-8 -*-
  2. import struct
  3. import traceback
  4. from time import time as now
  5. from collections import namedtuple
  6. from ._keyboard_event import KeyboardEvent, KEY_DOWN, KEY_UP, normalize_name
  7. from ._nixcommon import EV_KEY, aggregate_devices, ensure_root
  8. from ._suppress import KeyTable
  9. # TODO: start by reading current keyboard state, as to not missing any already pressed keys.
  10. # See: http://stackoverflow.com/questions/3649874/how-to-get-keyboard-state-in-linux
  11. def cleanup_key(name):
  12. """ Formats a dumpkeys format to our standard. """
  13. name = name.lstrip('+')
  14. is_keypad = name.startswith('KP_')
  15. for mod in ('Meta_', 'Control_', 'dead_', 'KP_'):
  16. if name.startswith(mod):
  17. name = name[len(mod):]
  18. # Dumpkeys is weird like that.
  19. if name == 'Remove':
  20. name = 'Delete'
  21. elif name == 'Delete':
  22. name = 'Backspace'
  23. return normalize_name(name), is_keypad
  24. def cleanup_modifier(modifier):
  25. expected = ('alt', 'ctrl', 'shift', 'alt gr')
  26. modifier = normalize_name(modifier)
  27. if modifier in expected:
  28. return modifier
  29. if modifier[:-1] in expected:
  30. return modifier[:-1]
  31. raise ValueError('Unknown modifier {}'.format(modifier))
  32. """
  33. Use `dumpkeys --keys-only` to list all scan codes and their names. We
  34. then parse the output and built a table. For each scan code and modifiers we
  35. have a list of names and vice-versa.
  36. """
  37. from subprocess import check_output
  38. import re
  39. to_name = {}
  40. from_name = {}
  41. keypad_scan_codes = set()
  42. def register_key(key_and_modifiers, name):
  43. to_name[key_and_modifiers] = name
  44. from_name[name] = key_and_modifiers
  45. def build_tables():
  46. if to_name and from_name: return
  47. ensure_root()
  48. keycode_template = r'^(.*?)keycode\s+(\d+)\s+=(.*?)$'
  49. dump = check_output(['dumpkeys', '--keys-only'], universal_newlines=True)
  50. for str_modifiers, str_scan_code, str_names in re.findall(keycode_template, dump, re.MULTILINE):
  51. if not str_names: continue
  52. modifiers = tuple(sorted(set(cleanup_modifier(m) for m in str_modifiers.strip().split())))
  53. scan_code = int(str_scan_code)
  54. name, is_keypad = cleanup_key(str_names.strip().split()[0])
  55. to_name[(scan_code, modifiers)] = name
  56. if is_keypad:
  57. keypad_scan_codes.add(scan_code)
  58. from_name['keypad ' + name] = (scan_code, ())
  59. if name not in from_name or len(modifiers) < len(from_name[name][1]):
  60. from_name[name] = (scan_code, modifiers)
  61. # Assume Shift uppercases keys that are single characters.
  62. # Hackish, but a good heuristic so far.
  63. for name, (scan_code, modifiers) in list(from_name.items()):
  64. upper = name.upper()
  65. if len(name) == 1 and upper not in from_name:
  66. register_key((scan_code, modifiers + ('shift',)), upper)
  67. # dumpkeys consistently misreports the Windows key, sometimes
  68. # skipping it completely or reporting as 'alt. 125 = left win,
  69. # 126 = right win.
  70. if (125, ()) not in to_name or to_name[(125, ())] == 'alt':
  71. register_key((125, ()), 'windows')
  72. if (126, ()) not in to_name or to_name[(126, ())] == 'alt':
  73. register_key((126, ()), 'windows')
  74. # The menu key is usually skipped altogether, so we also add it manually.
  75. if (127, ()) not in to_name:
  76. register_key((127, ()), 'menu')
  77. synonyms_template = r'^(\S+)\s+for (.+)$'
  78. dump = check_output(['dumpkeys', '--long-info'], universal_newlines=True)
  79. for synonym_str, original_str in re.findall(synonyms_template, dump, re.MULTILINE):
  80. synonym, _ = cleanup_key(synonym_str)
  81. original, _ = cleanup_key(original_str)
  82. try:
  83. from_name[synonym] = from_name[original]
  84. except KeyError:
  85. # Dumpkeys reported a synonym to an unknown key.
  86. pass
  87. device = None
  88. def build_device():
  89. global device
  90. if device: return
  91. ensure_root()
  92. device = aggregate_devices('kbd')
  93. def init():
  94. build_device()
  95. build_tables()
  96. pressed_modifiers = set()
  97. def listen(queue, is_allowed=lambda *args: True):
  98. build_device()
  99. build_tables()
  100. while True:
  101. time, type, code, value, device_id = device.read_event()
  102. if type != EV_KEY:
  103. continue
  104. scan_code = code
  105. event_type = KEY_DOWN if value else KEY_UP # 0 = UP, 1 = DOWN, 2 = HOLD
  106. try:
  107. name = to_name[(scan_code, tuple(sorted(pressed_modifiers)))]
  108. except KeyError:
  109. name = to_name.get((scan_code, ()), 'unknown')
  110. if name in ('alt', 'alt gr', 'ctrl', 'shift'):
  111. if event_type == KEY_DOWN:
  112. pressed_modifiers.add(name)
  113. else:
  114. pressed_modifiers.discard(name)
  115. is_keypad = scan_code in keypad_scan_codes
  116. queue.put(KeyboardEvent(event_type=event_type, scan_code=scan_code, name=name, time=time, device=device_id, is_keypad=is_keypad))
  117. def write_event(scan_code, is_down):
  118. build_device()
  119. device.write_event(EV_KEY, scan_code, int(is_down))
  120. def map_char(name):
  121. build_tables()
  122. if name in from_name:
  123. return from_name[name]
  124. parts = name.split(' ', 1)
  125. if (name.startswith('left ') or name.startswith('right ')) and parts[1] in from_name:
  126. return from_name[parts[1]]
  127. else:
  128. raise ValueError('Name {} is not mapped to any known key.'.format(repr(name)))
  129. def press(scan_code):
  130. write_event(scan_code, True)
  131. def release(scan_code):
  132. write_event(scan_code, False)
  133. def type_unicode(character):
  134. codepoint = ord(character)
  135. hexadecimal = hex(codepoint)[len('0x'):]
  136. for key in ['ctrl', 'shift', 'u']:
  137. scan_code, _ = map_char(key)
  138. press(scan_code)
  139. for key in hexadecimal:
  140. scan_code, _ = map_char(key)
  141. press(scan_code)
  142. release(scan_code)
  143. for key in ['ctrl', 'shift', 'u']:
  144. scan_code, _ = map_char(key)
  145. release(scan_code)
  146. if __name__ == '__main__':
  147. def p(e):
  148. print(e)
  149. listen(p)