win32_input.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. from __future__ import unicode_literals
  2. from ctypes import windll, pointer
  3. from ctypes.wintypes import DWORD, HANDLE
  4. from six.moves import range
  5. from prompt_toolkit.key_binding.input_processor import KeyPress
  6. from prompt_toolkit.keys import Keys
  7. from prompt_toolkit.mouse_events import MouseEventType
  8. from prompt_toolkit.win32_types import EventTypes, KEY_EVENT_RECORD, MOUSE_EVENT_RECORD, INPUT_RECORD, STD_INPUT_HANDLE
  9. import msvcrt
  10. import os
  11. import sys
  12. import six
  13. __all__ = (
  14. 'ConsoleInputReader',
  15. 'raw_mode',
  16. 'cooked_mode'
  17. )
  18. class ConsoleInputReader(object):
  19. """
  20. :param recognize_paste: When True, try to discover paste actions and turn
  21. the event into a BracketedPaste.
  22. """
  23. # Keys with character data.
  24. mappings = {
  25. b'\x1b': Keys.Escape,
  26. b'\x00': Keys.ControlSpace, # Control-Space (Also for Ctrl-@)
  27. b'\x01': Keys.ControlA, # Control-A (home)
  28. b'\x02': Keys.ControlB, # Control-B (emacs cursor left)
  29. b'\x03': Keys.ControlC, # Control-C (interrupt)
  30. b'\x04': Keys.ControlD, # Control-D (exit)
  31. b'\x05': Keys.ControlE, # Contrel-E (end)
  32. b'\x06': Keys.ControlF, # Control-F (cursor forward)
  33. b'\x07': Keys.ControlG, # Control-G
  34. b'\x08': Keys.ControlH, # Control-H (8) (Identical to '\b')
  35. b'\x09': Keys.ControlI, # Control-I (9) (Identical to '\t')
  36. b'\x0a': Keys.ControlJ, # Control-J (10) (Identical to '\n')
  37. b'\x0b': Keys.ControlK, # Control-K (delete until end of line; vertical tab)
  38. b'\x0c': Keys.ControlL, # Control-L (clear; form feed)
  39. b'\x0d': Keys.ControlJ, # Control-J NOTE: Windows sends \r instead of
  40. # \n when pressing enter. We turn it into \n
  41. # to be compatible with other platforms.
  42. b'\x0e': Keys.ControlN, # Control-N (14) (history forward)
  43. b'\x0f': Keys.ControlO, # Control-O (15)
  44. b'\x10': Keys.ControlP, # Control-P (16) (history back)
  45. b'\x11': Keys.ControlQ, # Control-Q
  46. b'\x12': Keys.ControlR, # Control-R (18) (reverse search)
  47. b'\x13': Keys.ControlS, # Control-S (19) (forward search)
  48. b'\x14': Keys.ControlT, # Control-T
  49. b'\x15': Keys.ControlU, # Control-U
  50. b'\x16': Keys.ControlV, # Control-V
  51. b'\x17': Keys.ControlW, # Control-W
  52. b'\x18': Keys.ControlX, # Control-X
  53. b'\x19': Keys.ControlY, # Control-Y (25)
  54. b'\x1a': Keys.ControlZ, # Control-Z
  55. b'\x1c': Keys.ControlBackslash, # Both Control-\ and Ctrl-|
  56. b'\x1d': Keys.ControlSquareClose, # Control-]
  57. b'\x1e': Keys.ControlCircumflex, # Control-^
  58. b'\x1f': Keys.ControlUnderscore, # Control-underscore (Also for Ctrl-hypen.)
  59. b'\x7f': Keys.Backspace, # (127) Backspace
  60. }
  61. # Keys that don't carry character data.
  62. keycodes = {
  63. # Home/End
  64. 33: Keys.PageUp,
  65. 34: Keys.PageDown,
  66. 35: Keys.End,
  67. 36: Keys.Home,
  68. # Arrows
  69. 37: Keys.Left,
  70. 38: Keys.Up,
  71. 39: Keys.Right,
  72. 40: Keys.Down,
  73. 45: Keys.Insert,
  74. 46: Keys.Delete,
  75. # F-keys.
  76. 112: Keys.F1,
  77. 113: Keys.F2,
  78. 114: Keys.F3,
  79. 115: Keys.F4,
  80. 116: Keys.F5,
  81. 117: Keys.F6,
  82. 118: Keys.F7,
  83. 119: Keys.F8,
  84. 120: Keys.F9,
  85. 121: Keys.F10,
  86. 122: Keys.F11,
  87. 123: Keys.F12,
  88. }
  89. LEFT_ALT_PRESSED = 0x0002
  90. RIGHT_ALT_PRESSED = 0x0001
  91. SHIFT_PRESSED = 0x0010
  92. LEFT_CTRL_PRESSED = 0x0008
  93. RIGHT_CTRL_PRESSED = 0x0004
  94. def __init__(self, recognize_paste=True):
  95. self._fdcon = None
  96. self.recognize_paste = recognize_paste
  97. # When stdin is a tty, use that handle, otherwise, create a handle from
  98. # CONIN$.
  99. if sys.stdin.isatty():
  100. self.handle = HANDLE(windll.kernel32.GetStdHandle(STD_INPUT_HANDLE))
  101. else:
  102. self._fdcon = os.open('CONIN$', os.O_RDWR | os.O_BINARY)
  103. self.handle = HANDLE(msvcrt.get_osfhandle(self._fdcon))
  104. def close(self):
  105. " Close fdcon. "
  106. if self._fdcon is not None:
  107. os.close(self._fdcon)
  108. def read(self):
  109. """
  110. Return a list of `KeyPress` instances. It won't return anything when
  111. there was nothing to read. (This function doesn't block.)
  112. http://msdn.microsoft.com/en-us/library/windows/desktop/ms684961(v=vs.85).aspx
  113. """
  114. max_count = 2048 # Max events to read at the same time.
  115. read = DWORD(0)
  116. arrtype = INPUT_RECORD * max_count
  117. input_records = arrtype()
  118. # Get next batch of input event.
  119. windll.kernel32.ReadConsoleInputW(
  120. self.handle, pointer(input_records), max_count, pointer(read))
  121. # First, get all the keys from the input buffer, in order to determine
  122. # whether we should consider this a paste event or not.
  123. all_keys = list(self._get_keys(read, input_records))
  124. if self.recognize_paste and self._is_paste(all_keys):
  125. gen = iter(all_keys)
  126. for k in gen:
  127. # Pasting: if the current key consists of text or \n, turn it
  128. # into a BracketedPaste.
  129. data = []
  130. while k and (isinstance(k.key, six.text_type) or
  131. k.key == Keys.ControlJ):
  132. data.append(k.data)
  133. try:
  134. k = next(gen)
  135. except StopIteration:
  136. k = None
  137. if data:
  138. yield KeyPress(Keys.BracketedPaste, ''.join(data))
  139. if k is not None:
  140. yield k
  141. else:
  142. for k in all_keys:
  143. yield k
  144. def _get_keys(self, read, input_records):
  145. """
  146. Generator that yields `KeyPress` objects from the input records.
  147. """
  148. for i in range(read.value):
  149. ir = input_records[i]
  150. # Get the right EventType from the EVENT_RECORD.
  151. # (For some reason the Windows console application 'cmder'
  152. # [http://gooseberrycreative.com/cmder/] can return '0' for
  153. # ir.EventType. -- Just ignore that.)
  154. if ir.EventType in EventTypes:
  155. ev = getattr(ir.Event, EventTypes[ir.EventType])
  156. # Process if this is a key event. (We also have mouse, menu and
  157. # focus events.)
  158. if type(ev) == KEY_EVENT_RECORD and ev.KeyDown:
  159. for key_press in self._event_to_key_presses(ev):
  160. yield key_press
  161. elif type(ev) == MOUSE_EVENT_RECORD:
  162. for key_press in self._handle_mouse(ev):
  163. yield key_press
  164. @staticmethod
  165. def _is_paste(keys):
  166. """
  167. Return `True` when we should consider this list of keys as a paste
  168. event. Pasted text on windows will be turned into a
  169. `Keys.BracketedPaste` event. (It's not 100% correct, but it is probably
  170. the best possible way to detect pasting of text and handle that
  171. correctly.)
  172. """
  173. # Consider paste when it contains at least one newline and at least one
  174. # other character.
  175. text_count = 0
  176. newline_count = 0
  177. for k in keys:
  178. if isinstance(k.key, six.text_type):
  179. text_count += 1
  180. if k.key == Keys.ControlJ:
  181. newline_count += 1
  182. return newline_count >= 1 and text_count > 1
  183. def _event_to_key_presses(self, ev):
  184. """
  185. For this `KEY_EVENT_RECORD`, return a list of `KeyPress` instances.
  186. """
  187. assert type(ev) == KEY_EVENT_RECORD and ev.KeyDown
  188. result = None
  189. u_char = ev.uChar.UnicodeChar
  190. ascii_char = u_char.encode('utf-8')
  191. # NOTE: We don't use `ev.uChar.AsciiChar`. That appears to be latin-1
  192. # encoded. See also:
  193. # https://github.com/ipython/ipython/issues/10004
  194. # https://github.com/jonathanslenders/python-prompt-toolkit/issues/389
  195. if u_char == '\x00':
  196. if ev.VirtualKeyCode in self.keycodes:
  197. result = KeyPress(self.keycodes[ev.VirtualKeyCode], '')
  198. else:
  199. if ascii_char in self.mappings:
  200. if self.mappings[ascii_char] == Keys.ControlJ:
  201. u_char = '\n' # Windows sends \n, turn into \r for unix compatibility.
  202. result = KeyPress(self.mappings[ascii_char], u_char)
  203. else:
  204. result = KeyPress(u_char, u_char)
  205. # Correctly handle Control-Arrow keys.
  206. if (ev.ControlKeyState & self.LEFT_CTRL_PRESSED or
  207. ev.ControlKeyState & self.RIGHT_CTRL_PRESSED) and result:
  208. if result.key == Keys.Left:
  209. result.key = Keys.ControlLeft
  210. if result.key == Keys.Right:
  211. result.key = Keys.ControlRight
  212. if result.key == Keys.Up:
  213. result.key = Keys.ControlUp
  214. if result.key == Keys.Down:
  215. result.key = Keys.ControlDown
  216. # Turn 'Tab' into 'BackTab' when shift was pressed.
  217. if ev.ControlKeyState & self.SHIFT_PRESSED and result:
  218. if result.key == Keys.Tab:
  219. result.key = Keys.BackTab
  220. # Turn 'Space' into 'ControlSpace' when control was pressed.
  221. if (ev.ControlKeyState & self.LEFT_CTRL_PRESSED or
  222. ev.ControlKeyState & self.RIGHT_CTRL_PRESSED) and result and result.data == ' ':
  223. result = KeyPress(Keys.ControlSpace, ' ')
  224. # Turn Control-Enter into META-Enter. (On a vt100 terminal, we cannot
  225. # detect this combination. But it's really practical on Windows.)
  226. if (ev.ControlKeyState & self.LEFT_CTRL_PRESSED or
  227. ev.ControlKeyState & self.RIGHT_CTRL_PRESSED) and result and \
  228. result.key == Keys.ControlJ:
  229. return [KeyPress(Keys.Escape, ''), result]
  230. # Return result. If alt was pressed, prefix the result with an
  231. # 'Escape' key, just like unix VT100 terminals do.
  232. # NOTE: Only replace the left alt with escape. The right alt key often
  233. # acts as altgr and is used in many non US keyboard layouts for
  234. # typing some special characters, like a backslash. We don't want
  235. # all backslashes to be prefixed with escape. (Esc-\ has a
  236. # meaning in E-macs, for instance.)
  237. if result:
  238. meta_pressed = ev.ControlKeyState & self.LEFT_ALT_PRESSED
  239. if meta_pressed:
  240. return [KeyPress(Keys.Escape, ''), result]
  241. else:
  242. return [result]
  243. else:
  244. return []
  245. def _handle_mouse(self, ev):
  246. """
  247. Handle mouse events. Return a list of KeyPress instances.
  248. """
  249. FROM_LEFT_1ST_BUTTON_PRESSED = 0x1
  250. result = []
  251. # Check event type.
  252. if ev.ButtonState == FROM_LEFT_1ST_BUTTON_PRESSED:
  253. # On a key press, generate both the mouse down and up event.
  254. for event_type in [MouseEventType.MOUSE_DOWN, MouseEventType.MOUSE_UP]:
  255. data = ';'.join([
  256. event_type,
  257. str(ev.MousePosition.X),
  258. str(ev.MousePosition.Y)
  259. ])
  260. result.append(KeyPress(Keys.WindowsMouseEvent, data))
  261. return result
  262. class raw_mode(object):
  263. """
  264. ::
  265. with raw_mode(stdin):
  266. ''' the windows terminal is now in 'raw' mode. '''
  267. The ``fileno`` attribute is ignored. This is to be compatble with the
  268. `raw_input` method of `.vt100_input`.
  269. """
  270. def __init__(self, fileno=None):
  271. self.handle = HANDLE(windll.kernel32.GetStdHandle(STD_INPUT_HANDLE))
  272. def __enter__(self):
  273. # Remember original mode.
  274. original_mode = DWORD()
  275. windll.kernel32.GetConsoleMode(self.handle, pointer(original_mode))
  276. self.original_mode = original_mode
  277. self._patch()
  278. def _patch(self):
  279. # Set raw
  280. ENABLE_ECHO_INPUT = 0x0004
  281. ENABLE_LINE_INPUT = 0x0002
  282. ENABLE_PROCESSED_INPUT = 0x0001
  283. windll.kernel32.SetConsoleMode(
  284. self.handle, self.original_mode.value &
  285. ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT))
  286. def __exit__(self, *a, **kw):
  287. # Restore original mode
  288. windll.kernel32.SetConsoleMode(self.handle, self.original_mode)
  289. class cooked_mode(raw_mode):
  290. """
  291. ::
  292. with cooked_mode(stdin):
  293. ''' the pseudo-terminal stdin is now used in raw mode '''
  294. """
  295. def _patch(self):
  296. # Set cooked.
  297. ENABLE_ECHO_INPUT = 0x0004
  298. ENABLE_LINE_INPUT = 0x0002
  299. ENABLE_PROCESSED_INPUT = 0x0001
  300. windll.kernel32.SetConsoleMode(
  301. self.handle, self.original_mode.value |
  302. (ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT))