You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

243 lines
5.6 KiB

4 years ago
  1. 'use strict';
  2. const readline = require('readline');
  3. const combos = require('./combos');
  4. /* eslint-disable no-control-regex */
  5. const metaKeyCodeRe = /^(?:\x1b)([a-zA-Z0-9])$/;
  6. const fnKeyRe = /^(?:\x1b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/;
  7. const keyName = {
  8. /* xterm/gnome ESC O letter */
  9. 'OP': 'f1',
  10. 'OQ': 'f2',
  11. 'OR': 'f3',
  12. 'OS': 'f4',
  13. /* xterm/rxvt ESC [ number ~ */
  14. '[11~': 'f1',
  15. '[12~': 'f2',
  16. '[13~': 'f3',
  17. '[14~': 'f4',
  18. /* from Cygwin and used in libuv */
  19. '[[A': 'f1',
  20. '[[B': 'f2',
  21. '[[C': 'f3',
  22. '[[D': 'f4',
  23. '[[E': 'f5',
  24. /* common */
  25. '[15~': 'f5',
  26. '[17~': 'f6',
  27. '[18~': 'f7',
  28. '[19~': 'f8',
  29. '[20~': 'f9',
  30. '[21~': 'f10',
  31. '[23~': 'f11',
  32. '[24~': 'f12',
  33. /* xterm ESC [ letter */
  34. '[A': 'up',
  35. '[B': 'down',
  36. '[C': 'right',
  37. '[D': 'left',
  38. '[E': 'clear',
  39. '[F': 'end',
  40. '[H': 'home',
  41. /* xterm/gnome ESC O letter */
  42. 'OA': 'up',
  43. 'OB': 'down',
  44. 'OC': 'right',
  45. 'OD': 'left',
  46. 'OE': 'clear',
  47. 'OF': 'end',
  48. 'OH': 'home',
  49. /* xterm/rxvt ESC [ number ~ */
  50. '[1~': 'home',
  51. '[2~': 'insert',
  52. '[3~': 'delete',
  53. '[4~': 'end',
  54. '[5~': 'pageup',
  55. '[6~': 'pagedown',
  56. /* putty */
  57. '[[5~': 'pageup',
  58. '[[6~': 'pagedown',
  59. /* rxvt */
  60. '[7~': 'home',
  61. '[8~': 'end',
  62. /* rxvt keys with modifiers */
  63. '[a': 'up',
  64. '[b': 'down',
  65. '[c': 'right',
  66. '[d': 'left',
  67. '[e': 'clear',
  68. '[2$': 'insert',
  69. '[3$': 'delete',
  70. '[5$': 'pageup',
  71. '[6$': 'pagedown',
  72. '[7$': 'home',
  73. '[8$': 'end',
  74. 'Oa': 'up',
  75. 'Ob': 'down',
  76. 'Oc': 'right',
  77. 'Od': 'left',
  78. 'Oe': 'clear',
  79. '[2^': 'insert',
  80. '[3^': 'delete',
  81. '[5^': 'pageup',
  82. '[6^': 'pagedown',
  83. '[7^': 'home',
  84. '[8^': 'end',
  85. /* misc. */
  86. '[Z': 'tab',
  87. }
  88. function isShiftKey(code) {
  89. return ['[a', '[b', '[c', '[d', '[e', '[2$', '[3$', '[5$', '[6$', '[7$', '[8$', '[Z'].includes(code)
  90. }
  91. function isCtrlKey(code) {
  92. return [ 'Oa', 'Ob', 'Oc', 'Od', 'Oe', '[2^', '[3^', '[5^', '[6^', '[7^', '[8^'].includes(code)
  93. }
  94. const keypress = (s = '', event = {}) => {
  95. let parts;
  96. let key = {
  97. name: event.name,
  98. ctrl: false,
  99. meta: false,
  100. shift: false,
  101. option: false,
  102. sequence: s,
  103. raw: s,
  104. ...event
  105. };
  106. if (Buffer.isBuffer(s)) {
  107. if (s[0] > 127 && s[1] === void 0) {
  108. s[0] -= 128;
  109. s = '\x1b' + String(s);
  110. } else {
  111. s = String(s);
  112. }
  113. } else if (s !== void 0 && typeof s !== 'string') {
  114. s = String(s);
  115. } else if (!s) {
  116. s = key.sequence || '';
  117. }
  118. key.sequence = key.sequence || s || key.name;
  119. if (s === '\r') {
  120. // carriage return
  121. key.raw = void 0;
  122. key.name = 'return';
  123. } else if (s === '\n') {
  124. // enter, should have been called linefeed
  125. key.name = 'enter';
  126. } else if (s === '\t') {
  127. // tab
  128. key.name = 'tab';
  129. } else if (s === '\b' || s === '\x7f' || s === '\x1b\x7f' || s === '\x1b\b') {
  130. // backspace or ctrl+h
  131. key.name = 'backspace';
  132. key.meta = s.charAt(0) === '\x1b';
  133. } else if (s === '\x1b' || s === '\x1b\x1b') {
  134. // escape key
  135. key.name = 'escape';
  136. key.meta = s.length === 2;
  137. } else if (s === ' ' || s === '\x1b ') {
  138. key.name = 'space';
  139. key.meta = s.length === 2;
  140. } else if (s <= '\x1a') {
  141. // ctrl+letter
  142. key.name = String.fromCharCode(s.charCodeAt(0) + 'a'.charCodeAt(0) - 1);
  143. key.ctrl = true;
  144. } else if (s.length === 1 && s >= '0' && s <= '9') {
  145. // number
  146. key.name = 'number';
  147. } else if (s.length === 1 && s >= 'a' && s <= 'z') {
  148. // lowercase letter
  149. key.name = s;
  150. } else if (s.length === 1 && s >= 'A' && s <= 'Z') {
  151. // shift+letter
  152. key.name = s.toLowerCase();
  153. key.shift = true;
  154. } else if ((parts = metaKeyCodeRe.exec(s))) {
  155. // meta+character key
  156. key.meta = true;
  157. key.shift = /^[A-Z]$/.test(parts[1]);
  158. } else if ((parts = fnKeyRe.exec(s))) {
  159. let segs = [...s];
  160. if (segs[0] === '\u001b' && segs[1] === '\u001b') {
  161. key.option = true;
  162. }
  163. // ansi escape sequence
  164. // reassemble the key code leaving out leading \x1b's,
  165. // the modifier key bitflag and any meaningless "1;" sequence
  166. let code = [parts[1], parts[2], parts[4], parts[6]].filter(Boolean).join('');
  167. let modifier = (parts[3] || parts[5] || 1) - 1;
  168. // Parse the key modifier
  169. key.ctrl = !!(modifier & 4);
  170. key.meta = !!(modifier & 10);
  171. key.shift = !!(modifier & 1);
  172. key.code = code;
  173. key.name = keyName[code];
  174. key.shift = isShiftKey(code) || key.shift;
  175. key.ctrl = isCtrlKey(code) || key.ctrl;
  176. }
  177. return key;
  178. };
  179. keypress.listen = (options = {}, onKeypress) => {
  180. let { stdin } = options;
  181. if (!stdin || (stdin !== process.stdin && !stdin.isTTY)) {
  182. throw new Error('Invalid stream passed');
  183. }
  184. let rl = readline.createInterface({ terminal: true, input: stdin });
  185. readline.emitKeypressEvents(stdin, rl);
  186. let on = (buf, key) => onKeypress(buf, keypress(buf, key), rl);
  187. let isRaw = stdin.isRaw;
  188. if (stdin.isTTY) stdin.setRawMode(true);
  189. stdin.on('keypress', on);
  190. rl.resume();
  191. let off = () => {
  192. if (stdin.isTTY) stdin.setRawMode(isRaw);
  193. stdin.removeListener('keypress', on);
  194. rl.pause();
  195. rl.close();
  196. };
  197. return off;
  198. };
  199. keypress.action = (buf, key, customActions) => {
  200. let obj = { ...combos, ...customActions };
  201. if (key.ctrl) {
  202. key.action = obj.ctrl[key.name];
  203. return key;
  204. }
  205. if (key.option && obj.option) {
  206. key.action = obj.option[key.name];
  207. return key;
  208. }
  209. if (key.shift) {
  210. key.action = obj.shift[key.name];
  211. return key;
  212. }
  213. key.action = obj.keys[key.name];
  214. return key;
  215. };
  216. module.exports = keypress;