_darwinkeyboard.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. import ctypes
  2. import ctypes.util
  3. import Quartz
  4. import time
  5. import os
  6. import threading
  7. from AppKit import NSEvent
  8. from ._keyboard_event import KeyboardEvent, KEY_DOWN, KEY_UP, normalize_name
  9. try: # Python 2/3 compatibility
  10. unichr
  11. except NameError:
  12. unichr = chr
  13. Carbon = ctypes.cdll.LoadLibrary(ctypes.util.find_library('Carbon'))
  14. class KeyMap(object):
  15. non_layout_keys = {
  16. 0x24: 'return',
  17. 0x30: 'tab',
  18. 0x31: 'space',
  19. 0x33: 'delete',
  20. 0x35: 'esc',
  21. 0x36: 'right command',
  22. 0x37: 'command',
  23. 0x38: 'shift',
  24. 0x39: 'caps lock',
  25. 0x3A: 'alt',
  26. 0x3B: 'ctrl',
  27. 0x3C: 'right shift',
  28. 0x3D: 'right option',
  29. 0x3E: 'right control',
  30. 0x3F: 'function',
  31. 0x7E: 'up', # This and below might be layout-specific, but they have
  32. 0x7D: 'down', # been giving me headaches trying to locate so I am hard-
  33. 0x7B: 'left', # coding instead. The unicode character generated by
  34. 0x7C: 'right', # UCKeyTranslate does not match the constants provided by
  35. 0x73: 'home', # Quartz.
  36. 0x77: 'end',
  37. 0x74: 'page up',
  38. 0x79: 'page down',
  39. }
  40. layout_specific_keys = {}
  41. def __init__(self):
  42. # Virtual key codes are usually the same for any given key, unless you have a different
  43. # keyboard layout. The only way I've found to determine the layout relies on (supposedly
  44. # deprecated) Carbon APIs. If there's a more modern way to do this, please update this
  45. # section.
  46. # Set up data types and exported values:
  47. CFTypeRef = ctypes.c_void_p
  48. CFDataRef = ctypes.c_void_p
  49. CFIndex = ctypes.c_uint64
  50. OptionBits = ctypes.c_uint32
  51. UniCharCount = ctypes.c_uint8
  52. UniChar = ctypes.c_uint16
  53. UniChar4 = UniChar * 4
  54. class CFRange(ctypes.Structure):
  55. _fields_ = [('loc', CFIndex),
  56. ('len', CFIndex)]
  57. kTISPropertyUnicodeKeyLayoutData = ctypes.c_void_p.in_dll(Carbon, 'kTISPropertyUnicodeKeyLayoutData')
  58. shiftKey = 0x0200
  59. alphaKey = 0x0400
  60. optionKey = 0x0800
  61. controlKey = 0x1000
  62. kUCKeyActionDisplay = 3
  63. kUCKeyTranslateNoDeadKeysBit = 0
  64. # Set up function calls:
  65. Carbon.CFDataGetBytes.argtypes = [CFDataRef] #, CFRange, UInt8
  66. Carbon.CFDataGetBytes.restype = None
  67. Carbon.CFDataGetLength.argtypes = [CFDataRef]
  68. Carbon.CFDataGetLength.restype = CFIndex
  69. Carbon.CFRelease.argtypes = [CFTypeRef]
  70. Carbon.CFRelease.restype = None
  71. Carbon.LMGetKbdType.argtypes = []
  72. Carbon.LMGetKbdType.restype = ctypes.c_uint32
  73. Carbon.TISCopyCurrentKeyboardInputSource.argtypes = []
  74. Carbon.TISCopyCurrentKeyboardInputSource.restype = ctypes.c_void_p
  75. Carbon.TISGetInputSourceProperty.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
  76. Carbon.TISGetInputSourceProperty.restype = ctypes.c_void_p
  77. Carbon.UCKeyTranslate.argtypes = [ctypes.c_void_p,
  78. ctypes.c_uint16,
  79. ctypes.c_uint16,
  80. ctypes.c_uint32,
  81. ctypes.c_uint32,
  82. OptionBits, # keyTranslateOptions
  83. ctypes.POINTER(ctypes.c_uint32), # deadKeyState
  84. UniCharCount, # maxStringLength
  85. ctypes.POINTER(UniCharCount), # actualStringLength
  86. UniChar4]
  87. Carbon.UCKeyTranslate.restype = ctypes.c_uint32
  88. # Get keyboard layout
  89. klis = Carbon.TISCopyCurrentKeyboardInputSource()
  90. k_layout = Carbon.TISGetInputSourceProperty(klis, kTISPropertyUnicodeKeyLayoutData)
  91. k_layout_size = Carbon.CFDataGetLength(k_layout)
  92. k_layout_buffer = ctypes.create_string_buffer(k_layout_size) # TODO - Verify this works instead of initializing with empty string
  93. Carbon.CFDataGetBytes(k_layout, CFRange(0, k_layout_size), ctypes.byref(k_layout_buffer))
  94. # Generate character representations of key codes
  95. for key_code in range(0, 128):
  96. # TODO - Possibly add alt modifier to key map
  97. non_shifted_char = UniChar4()
  98. shifted_char = UniChar4()
  99. keys_down = ctypes.c_uint32()
  100. char_count = UniCharCount()
  101. retval = Carbon.UCKeyTranslate(k_layout_buffer,
  102. key_code,
  103. kUCKeyActionDisplay,
  104. 0, # No modifier
  105. Carbon.LMGetKbdType(),
  106. kUCKeyTranslateNoDeadKeysBit,
  107. ctypes.byref(keys_down),
  108. 4,
  109. ctypes.byref(char_count),
  110. non_shifted_char)
  111. non_shifted_key = u''.join(unichr(non_shifted_char[i]) for i in range(char_count.value))
  112. retval = Carbon.UCKeyTranslate(k_layout_buffer,
  113. key_code,
  114. kUCKeyActionDisplay,
  115. shiftKey >> 8, # Shift
  116. Carbon.LMGetKbdType(),
  117. kUCKeyTranslateNoDeadKeysBit,
  118. ctypes.byref(keys_down),
  119. 4,
  120. ctypes.byref(char_count),
  121. shifted_char)
  122. shifted_key = u''.join(unichr(shifted_char[i]) for i in range(char_count.value))
  123. self.layout_specific_keys[key_code] = (non_shifted_key, shifted_key)
  124. # Cleanup
  125. Carbon.CFRelease(klis)
  126. def character_to_vk(self, character):
  127. """ Returns a tuple of (scan_code, modifiers) where ``scan_code`` is a numeric scan code
  128. and ``modifiers`` is an array of string modifier names (like 'shift') """
  129. for vk in self.non_layout_keys:
  130. if self.non_layout_keys[vk] == character.lower():
  131. return (vk, [])
  132. for vk in self.layout_specific_keys:
  133. if self.layout_specific_keys[vk][0] == character:
  134. return (vk, [])
  135. elif self.layout_specific_keys[vk][1] == character:
  136. return (vk, ['shift'])
  137. raise ValueError("Unrecognized character: {}".format(character))
  138. def vk_to_character(self, vk, modifiers=[]):
  139. """ Returns a character corresponding to the specified scan code (with given
  140. modifiers applied) """
  141. if vk in self.non_layout_keys:
  142. # Not a character
  143. return self.non_layout_keys[vk]
  144. elif vk in self.layout_specific_keys:
  145. if 'shift' in modifiers:
  146. return self.layout_specific_keys[vk][1]
  147. return self.layout_specific_keys[vk][0]
  148. else:
  149. # Invalid vk
  150. raise ValueError("Invalid scan code: {}".format(vk))
  151. class KeyController(object):
  152. def __init__(self):
  153. self.key_map = KeyMap()
  154. self.current_modifiers = {
  155. "shift": False,
  156. "caps": False,
  157. "alt": False,
  158. "ctrl": False,
  159. "cmd": False,
  160. }
  161. self.media_keys = {
  162. 'KEYTYPE_SOUND_UP': 0,
  163. 'KEYTYPE_SOUND_DOWN': 1,
  164. 'KEYTYPE_BRIGHTNESS_UP': 2,
  165. 'KEYTYPE_BRIGHTNESS_DOWN': 3,
  166. 'KEYTYPE_CAPS_LOCK': 4,
  167. 'KEYTYPE_HELP': 5,
  168. 'POWER_KEY': 6,
  169. 'KEYTYPE_MUTE': 7,
  170. 'UP_ARROW_KEY': 8,
  171. 'DOWN_ARROW_KEY': 9,
  172. 'KEYTYPE_NUM_LOCK': 10,
  173. 'KEYTYPE_CONTRAST_UP': 11,
  174. 'KEYTYPE_CONTRAST_DOWN': 12,
  175. 'KEYTYPE_LAUNCH_PANEL': 13,
  176. 'KEYTYPE_EJECT': 14,
  177. 'KEYTYPE_VIDMIRROR': 15,
  178. 'KEYTYPE_PLAY': 16,
  179. 'KEYTYPE_NEXT': 17,
  180. 'KEYTYPE_PREVIOUS': 18,
  181. 'KEYTYPE_FAST': 19,
  182. 'KEYTYPE_REWIND': 20,
  183. 'KEYTYPE_ILLUMINATION_UP': 21,
  184. 'KEYTYPE_ILLUMINATION_DOWN': 22,
  185. 'KEYTYPE_ILLUMINATION_TOGGLE': 23
  186. }
  187. def press(self, key_code):
  188. """ Sends a 'down' event for the specified scan code """
  189. if key_code >= 128:
  190. # Media key
  191. ev = NSEvent.otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2_(
  192. 14, # type
  193. (0, 0), # location
  194. 0xa00, # flags
  195. 0, # timestamp
  196. 0, # window
  197. 0, # ctx
  198. 8, # subtype
  199. ((key_code-128) << 16) | (0xa << 8), # data1
  200. -1 # data2
  201. )
  202. Quartz.CGEventPost(0, ev.CGEvent())
  203. else:
  204. # Regular key
  205. # Apply modifiers if necessary
  206. event_flags = 0
  207. if self.current_modifiers["shift"]:
  208. event_flags += Quartz.kCGEventFlagMaskShift
  209. if self.current_modifiers["caps"]:
  210. event_flags += Quartz.kCGEventFlagMaskAlphaShift
  211. if self.current_modifiers["alt"]:
  212. event_flags += Quartz.kCGEventFlagMaskAlternate
  213. if self.current_modifiers["ctrl"]:
  214. event_flags += Quartz.kCGEventFlagMaskControl
  215. if self.current_modifiers["cmd"]:
  216. event_flags += Quartz.kCGEventFlagMaskCommand
  217. # Update modifiers if necessary
  218. if key_code == 0x37: # cmd
  219. self.current_modifiers["cmd"] = True
  220. elif key_code == 0x38: # shift
  221. self.current_modifiers["shift"] = True
  222. elif key_code == 0x39: # caps lock
  223. self.current_modifiers["caps"] = True
  224. elif key_code == 0x3A: # alt
  225. self.current_modifiers["alt"] = True
  226. elif key_code == 0x3B: # ctrl
  227. self.current_modifiers["ctrl"] = True
  228. event = Quartz.CGEventCreateKeyboardEvent(None, key_code, True)
  229. Quartz.CGEventSetFlags(event, event_flags)
  230. Quartz.CGEventPost(Quartz.kCGHIDEventTap, event)
  231. time.sleep(0.01)
  232. def release(self, key_code):
  233. """ Sends an 'up' event for the specified scan code """
  234. if key_code >= 128:
  235. # Media key
  236. ev = NSEvent.otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2_(
  237. 14, # type
  238. (0, 0), # location
  239. 0xb00, # flags
  240. 0, # timestamp
  241. 0, # window
  242. 0, # ctx
  243. 8, # subtype
  244. ((key_code-128) << 16) | (0xb << 8), # data1
  245. -1 # data2
  246. )
  247. Quartz.CGEventPost(0, ev.CGEvent())
  248. else:
  249. # Regular key
  250. # Update modifiers if necessary
  251. if key_code == 0x37: # cmd
  252. self.current_modifiers["cmd"] = False
  253. elif key_code == 0x38: # shift
  254. self.current_modifiers["shift"] = False
  255. elif key_code == 0x39: # caps lock
  256. self.current_modifiers["caps"] = False
  257. elif key_code == 0x3A: # alt
  258. self.current_modifiers["alt"] = False
  259. elif key_code == 0x3B: # ctrl
  260. self.current_modifiers["ctrl"] = False
  261. # Apply modifiers if necessary
  262. event_flags = 0
  263. if self.current_modifiers["shift"]:
  264. event_flags += Quartz.kCGEventFlagMaskShift
  265. if self.current_modifiers["caps"]:
  266. event_flags += Quartz.kCGEventFlagMaskAlphaShift
  267. if self.current_modifiers["alt"]:
  268. event_flags += Quartz.kCGEventFlagMaskAlternate
  269. if self.current_modifiers["ctrl"]:
  270. event_flags += Quartz.kCGEventFlagMaskControl
  271. if self.current_modifiers["cmd"]:
  272. event_flags += Quartz.kCGEventFlagMaskCommand
  273. event = Quartz.CGEventCreateKeyboardEvent(None, key_code, False)
  274. Quartz.CGEventSetFlags(event, event_flags)
  275. Quartz.CGEventPost(Quartz.kCGHIDEventTap, event)
  276. time.sleep(0.01)
  277. def map_char(self, character):
  278. if character in self.media_keys:
  279. return (128+self.media_keys[character],[])
  280. else:
  281. return self.key_map.character_to_vk(character)
  282. def map_scan_code(self, scan_code):
  283. if scan_code >= 128:
  284. character = [k for k, v in enumerate(self.media_keys) if v == scan_code-128]
  285. if len(character):
  286. return character[0]
  287. return None
  288. else:
  289. return self.key_map.vk_to_character(scan_code)
  290. class KeyEventListener(object):
  291. def __init__(self, callback, blocking=False):
  292. self.blocking = blocking
  293. self.callback = callback
  294. self.listening = True
  295. self.tap = None
  296. def run(self):
  297. """ Creates a listener and loops while waiting for an event. Intended to run as
  298. a background thread. """
  299. self.tap = Quartz.CGEventTapCreate(
  300. Quartz.kCGSessionEventTap,
  301. Quartz.kCGHeadInsertEventTap,
  302. Quartz.kCGEventTapOptionDefault,
  303. Quartz.CGEventMaskBit(Quartz.kCGEventKeyDown) |
  304. Quartz.CGEventMaskBit(Quartz.kCGEventKeyUp) |
  305. Quartz.CGEventMaskBit(Quartz.kCGEventFlagsChanged),
  306. self.handler,
  307. None)
  308. loopsource = Quartz.CFMachPortCreateRunLoopSource(None, self.tap, 0)
  309. loop = Quartz.CFRunLoopGetCurrent()
  310. Quartz.CFRunLoopAddSource(loop, loopsource, Quartz.kCFRunLoopDefaultMode)
  311. Quartz.CGEventTapEnable(self.tap, True)
  312. while self.listening:
  313. Quartz.CFRunLoopRunInMode(Quartz.kCFRunLoopDefaultMode, 5, False)
  314. def handler(self, proxy, e_type, event, refcon):
  315. scan_code = Quartz.CGEventGetIntegerValueField(event, Quartz.kCGKeyboardEventKeycode)
  316. key_name = name_from_scancode(scan_code)
  317. flags = Quartz.CGEventGetFlags(event)
  318. event_type = ""
  319. is_keypad = (flags & Quartz.kCGEventFlagMaskNumericPad)
  320. if e_type == Quartz.kCGEventKeyDown:
  321. event_type = "down"
  322. elif e_type == Quartz.kCGEventKeyUp:
  323. event_type = "up"
  324. elif e_type == Quartz.kCGEventFlagsChanged:
  325. if key_name.endswith("shift") and (flags & Quartz.kCGEventFlagMaskShift):
  326. event_type = "down"
  327. elif key_name == "caps lock" and (flags & Quartz.kCGEventFlagMaskAlphaShift):
  328. event_type = "down"
  329. elif (key_name.endswith("option") or key_name.endswith("alt")) and (flags & Quartz.kCGEventFlagMaskAlternate):
  330. event_type = "down"
  331. elif key_name == "ctrl" and (flags & Quartz.kCGEventFlagMaskControl):
  332. event_type = "down"
  333. elif key_name == "command" and (flags & Quartz.kCGEventFlagMaskCommand):
  334. event_type = "down"
  335. else:
  336. event_type = "up"
  337. if self.blocking:
  338. return None
  339. self.callback(KeyboardEvent(event_type, scan_code, name=key_name, is_keypad=is_keypad))
  340. return event
  341. key_controller = KeyController()
  342. """ Exported functions below """
  343. def init():
  344. key_controller = KeyController()
  345. def press(scan_code):
  346. """ Sends a 'down' event for the specified scan code """
  347. key_controller.press(scan_code)
  348. def release(scan_code):
  349. """ Sends an 'up' event for the specified scan code """
  350. key_controller.release(scan_code)
  351. def map_char(character):
  352. """ Returns a tuple of (scan_code, modifiers) where ``scan_code`` is a numeric scan code
  353. and ``modifiers`` is an array of string modifier names (like 'shift') """
  354. return key_controller.map_char(character)
  355. def name_from_scancode(scan_code):
  356. """ Returns the name or character associated with the specified key code """
  357. return key_controller.map_scan_code(scan_code)
  358. def listen(queue, is_allowed=lambda *args: True):
  359. """ Adds all monitored keyboard events to queue. To use the listener, the script must be run
  360. as root (administrator). Otherwise, it throws an OSError. """
  361. if not os.geteuid() == 0:
  362. raise OSError("Error 13 - Must be run as administrator")
  363. listener = KeyEventListener(lambda e: queue.put(e) or is_allowed(e.name, e.event_type == KEY_UP))
  364. t = threading.Thread(target=listener.run, args=())
  365. t.daemon = True
  366. t.start()