keymap.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. """Functions that help you work with QMK keymaps.
  2. """
  3. from pathlib import Path
  4. import json
  5. import subprocess
  6. from pygments.lexers.c_cpp import CLexer
  7. from pygments.token import Token
  8. from pygments import lex
  9. from milc import cli
  10. from qmk.keyboard import rules_mk
  11. import qmk.path
  12. import qmk.commands
  13. # The `keymap.c` template to use when a keyboard doesn't have its own
  14. DEFAULT_KEYMAP_C = """#include QMK_KEYBOARD_H
  15. /* THIS FILE WAS GENERATED!
  16. *
  17. * This file was generated by qmk json2c. You may or may not want to
  18. * edit it directly.
  19. */
  20. const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
  21. __KEYMAP_GOES_HERE__
  22. };
  23. """
  24. def template(keyboard, type='c'):
  25. """Returns the `keymap.c` or `keymap.json` template for a keyboard.
  26. If a template exists in `keyboards/<keyboard>/templates/keymap.c` that
  27. text will be used instead of `DEFAULT_KEYMAP_C`.
  28. If a template exists in `keyboards/<keyboard>/templates/keymap.json` that
  29. text will be used instead of an empty dictionary.
  30. Args:
  31. keyboard
  32. The keyboard to return a template for.
  33. type
  34. 'json' for `keymap.json` and 'c' (or anything else) for `keymap.c`
  35. """
  36. if type == 'json':
  37. template_file = Path('keyboards/%s/templates/keymap.json' % keyboard)
  38. template = {'keyboard': keyboard}
  39. if template_file.exists():
  40. template.update(json.loads(template_file.read_text()))
  41. else:
  42. template_file = Path('keyboards/%s/templates/keymap.c' % keyboard)
  43. if template_file.exists():
  44. template = template_file.read_text()
  45. else:
  46. template = DEFAULT_KEYMAP_C
  47. return template
  48. def _strip_any(keycode):
  49. """Remove ANY() from a keycode.
  50. """
  51. if keycode.startswith('ANY(') and keycode.endswith(')'):
  52. keycode = keycode[4:-1]
  53. return keycode
  54. def is_keymap_dir(keymap):
  55. """Return True if Path object `keymap` has a keymap file inside.
  56. """
  57. for file in ('keymap.c', 'keymap.json'):
  58. if (keymap / file).is_file():
  59. return True
  60. def generate(keyboard, layout, layers, type='c', keymap=None):
  61. """Returns a `keymap.c` or `keymap.json` for the specified keyboard, layout, and layers.
  62. Args:
  63. keyboard
  64. The name of the keyboard
  65. layout
  66. The LAYOUT macro this keymap uses.
  67. layers
  68. An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode.
  69. type
  70. 'json' for `keymap.json` and 'c' (or anything else) for `keymap.c`
  71. """
  72. new_keymap = template(keyboard, type)
  73. if type == 'json':
  74. new_keymap['keymap'] = keymap
  75. new_keymap['layout'] = layout
  76. new_keymap['layers'] = layers
  77. else:
  78. layer_txt = []
  79. for layer_num, layer in enumerate(layers):
  80. if layer_num != 0:
  81. layer_txt[-1] = layer_txt[-1] + ','
  82. layer = map(_strip_any, layer)
  83. layer_keys = ', '.join(layer)
  84. layer_txt.append('\t[%s] = %s(%s)' % (layer_num, layout, layer_keys))
  85. keymap = '\n'.join(layer_txt)
  86. new_keymap = new_keymap.replace('__KEYMAP_GOES_HERE__', keymap)
  87. return new_keymap
  88. def write(keyboard, keymap, layout, layers, type='c'):
  89. """Generate the `keymap.c` and write it to disk.
  90. Returns the filename written to.
  91. Args:
  92. keyboard
  93. The name of the keyboard
  94. keymap
  95. The name of the keymap
  96. layout
  97. The LAYOUT macro this keymap uses.
  98. layers
  99. An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode.
  100. type
  101. 'json' for `keymap.json` and 'c' (or anything else) for `keymap.c`
  102. """
  103. keymap_content = generate(keyboard, layout, layers, type)
  104. if type == 'json':
  105. keymap_file = qmk.path.keymap(keyboard) / keymap / 'keymap.json'
  106. keymap_content = json.dumps(keymap_content)
  107. else:
  108. keymap_file = qmk.path.keymap(keyboard) / keymap / 'keymap.c'
  109. keymap_file.parent.mkdir(parents=True, exist_ok=True)
  110. keymap_file.write_text(keymap_content)
  111. cli.log.info('Wrote keymap to {fg_cyan}%s', keymap_file)
  112. return keymap_file
  113. def locate_keymap(keyboard, keymap):
  114. """Returns the path to a keymap for a specific keyboard.
  115. """
  116. if not qmk.path.is_keyboard(keyboard):
  117. raise KeyError('Invalid keyboard: ' + repr(keyboard))
  118. # Check the keyboard folder first, last match wins
  119. checked_dirs = ''
  120. keymap_path = ''
  121. for dir in keyboard.split('/'):
  122. if checked_dirs:
  123. checked_dirs = '/'.join((checked_dirs, dir))
  124. else:
  125. checked_dirs = dir
  126. keymap_dir = Path('keyboards') / checked_dirs / 'keymaps'
  127. if (keymap_dir / keymap / 'keymap.c').exists():
  128. keymap_path = keymap_dir / keymap / 'keymap.c'
  129. if (keymap_dir / keymap / 'keymap.json').exists():
  130. keymap_path = keymap_dir / keymap / 'keymap.json'
  131. if keymap_path:
  132. return keymap_path
  133. # Check community layouts as a fallback
  134. rules = rules_mk(keyboard)
  135. if "LAYOUTS" in rules:
  136. for layout in rules["LAYOUTS"].split():
  137. community_layout = Path('layouts/community') / layout / keymap
  138. if community_layout.exists():
  139. if (community_layout / 'keymap.json').exists():
  140. return community_layout / 'keymap.json'
  141. if (community_layout / 'keymap.c').exists():
  142. return community_layout / 'keymap.c'
  143. def list_keymaps(keyboard):
  144. """ List the available keymaps for a keyboard.
  145. Args:
  146. keyboard: the keyboards full name with vendor and revision if necessary, example: clueboard/66/rev3
  147. Returns:
  148. a set with the names of the available keymaps
  149. """
  150. # parse all the rules.mk files for the keyboard
  151. rules = rules_mk(keyboard)
  152. names = set()
  153. if rules:
  154. # qmk_firmware/keyboards
  155. keyboards_dir = Path('keyboards')
  156. # path to the keyboard's directory
  157. kb_path = keyboards_dir / keyboard
  158. # walk up the directory tree until keyboards_dir
  159. # and collect all directories' name with keymap.c file in it
  160. while kb_path != keyboards_dir:
  161. keymaps_dir = kb_path / "keymaps"
  162. if keymaps_dir.exists():
  163. names = names.union([keymap.name for keymap in keymaps_dir.iterdir() if is_keymap_dir(keymap)])
  164. kb_path = kb_path.parent
  165. # if community layouts are supported, get them
  166. if "LAYOUTS" in rules:
  167. for layout in rules["LAYOUTS"].split():
  168. cl_path = Path('layouts/community') / layout
  169. if cl_path.exists():
  170. names = names.union([keymap.name for keymap in cl_path.iterdir() if is_keymap_dir(keymap)])
  171. return sorted(names)
  172. def _c_preprocess(path):
  173. """ Run a file through the C pre-processor
  174. Args:
  175. path: path of the keymap.c file
  176. Returns:
  177. the stdout of the pre-processor
  178. """
  179. pre_processed_keymap = qmk.commands.run(['cpp', path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
  180. return pre_processed_keymap.stdout
  181. def _get_layers(keymap): # noqa C901 : until someone has a good idea how to simplify/split up this code
  182. """ Find the layers in a keymap.c file.
  183. Args:
  184. keymap: the content of the keymap.c file
  185. Returns:
  186. a dictionary containing the parsed keymap
  187. """
  188. layers = list()
  189. opening_braces = '({['
  190. closing_braces = ')}]'
  191. keymap_certainty = brace_depth = 0
  192. is_keymap = is_layer = is_adv_kc = False
  193. layer = dict(name=False, layout=False, keycodes=list())
  194. for line in lex(keymap, CLexer()):
  195. if line[0] is Token.Name:
  196. if is_keymap:
  197. # If we are inside the keymap array
  198. # we know the keymap's name and the layout macro will come,
  199. # followed by the keycodes
  200. if not layer['name']:
  201. if line[1].startswith('LAYOUT') or line[1].startswith('KEYMAP'):
  202. # This can happen if the keymap array only has one layer,
  203. # for macropads and such
  204. layer['name'] = '0'
  205. layer['layout'] = line[1]
  206. else:
  207. layer['name'] = line[1]
  208. elif not layer['layout']:
  209. layer['layout'] = line[1]
  210. elif is_layer:
  211. # If we are inside a layout macro,
  212. # collect all keycodes
  213. if line[1] == '_______':
  214. kc = 'KC_TRNS'
  215. elif line[1] == 'XXXXXXX':
  216. kc = 'KC_NO'
  217. else:
  218. kc = line[1]
  219. if is_adv_kc:
  220. # If we are inside an advanced keycode
  221. # collect everything and hope the user
  222. # knew what he/she was doing
  223. layer['keycodes'][-1] += kc
  224. else:
  225. layer['keycodes'].append(kc)
  226. # The keymaps array's signature:
  227. # const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS]
  228. #
  229. # Only if we've found all 6 keywords in this specific order
  230. # can we know for sure that we are inside the keymaps array
  231. elif line[1] == 'PROGMEM' and keymap_certainty == 2:
  232. keymap_certainty = 3
  233. elif line[1] == 'keymaps' and keymap_certainty == 3:
  234. keymap_certainty = 4
  235. elif line[1] == 'MATRIX_ROWS' and keymap_certainty == 4:
  236. keymap_certainty = 5
  237. elif line[1] == 'MATRIX_COLS' and keymap_certainty == 5:
  238. keymap_certainty = 6
  239. elif line[0] is Token.Keyword:
  240. if line[1] == 'const' and keymap_certainty == 0:
  241. keymap_certainty = 1
  242. elif line[0] is Token.Keyword.Type:
  243. if line[1] == 'uint16_t' and keymap_certainty == 1:
  244. keymap_certainty = 2
  245. elif line[0] is Token.Punctuation:
  246. if line[1] in opening_braces:
  247. brace_depth += 1
  248. if is_keymap:
  249. if is_layer:
  250. # We found the beginning of a non-basic keycode
  251. is_adv_kc = True
  252. layer['keycodes'][-1] += line[1]
  253. elif line[1] == '(' and brace_depth == 2:
  254. # We found the beginning of a layer
  255. is_layer = True
  256. elif line[1] == '{' and keymap_certainty == 6:
  257. # We found the beginning of the keymaps array
  258. is_keymap = True
  259. elif line[1] in closing_braces:
  260. brace_depth -= 1
  261. if is_keymap:
  262. if is_adv_kc:
  263. layer['keycodes'][-1] += line[1]
  264. if brace_depth == 2:
  265. # We found the end of a non-basic keycode
  266. is_adv_kc = False
  267. elif line[1] == ')' and brace_depth == 1:
  268. # We found the end of a layer
  269. is_layer = False
  270. layers.append(layer)
  271. layer = dict(name=False, layout=False, keycodes=list())
  272. elif line[1] == '}' and brace_depth == 0:
  273. # We found the end of the keymaps array
  274. is_keymap = False
  275. keymap_certainty = 0
  276. elif is_adv_kc:
  277. # Advanced keycodes can contain other punctuation
  278. # e.g.: MT(MOD_LCTL | MOD_LSFT, KC_ESC)
  279. layer['keycodes'][-1] += line[1]
  280. elif line[0] is Token.Literal.Number.Integer and is_keymap and not is_adv_kc:
  281. # If the pre-processor finds the 'meaning' of the layer names,
  282. # they will be numbers
  283. if not layer['name']:
  284. layer['name'] = line[1]
  285. else:
  286. # We only care about
  287. # operators and such if we
  288. # are inside an advanced keycode
  289. # e.g.: MT(MOD_LCTL | MOD_LSFT, KC_ESC)
  290. if is_adv_kc:
  291. layer['keycodes'][-1] += line[1]
  292. return layers
  293. def parse_keymap_c(keymap_file, use_cpp=True):
  294. """ Parse a keymap.c file.
  295. Currently only cares about the keymaps array.
  296. Args:
  297. keymap_file: path of the keymap.c file
  298. use_cpp: if True, pre-process the file with the C pre-processor
  299. Returns:
  300. a dictionary containing the parsed keymap
  301. """
  302. if use_cpp:
  303. keymap_file = _c_preprocess(keymap_file)
  304. else:
  305. keymap_file = keymap_file.read_text()
  306. keymap = dict()
  307. keymap['layers'] = _get_layers(keymap_file)
  308. return keymap
  309. def c2json(keyboard, keymap, keymap_file, use_cpp=True):
  310. """ Convert keymap.c to keymap.json
  311. Args:
  312. keyboard: The name of the keyboard
  313. keymap: The name of the keymap
  314. layout: The LAYOUT macro this keymap uses.
  315. keymap_file: path of the keymap.c file
  316. use_cpp: if True, pre-process the file with the C pre-processor
  317. Returns:
  318. a dictionary in keymap.json format
  319. """
  320. keymap_json = parse_keymap_c(keymap_file, use_cpp)
  321. dirty_layers = keymap_json.pop('layers', None)
  322. keymap_json['layers'] = list()
  323. for layer in dirty_layers:
  324. layer.pop('name')
  325. layout = layer.pop('layout')
  326. if not keymap_json.get('layout', False):
  327. keymap_json['layout'] = layout
  328. keymap_json['layers'].append(layer.pop('keycodes'))
  329. keymap_json['keyboard'] = keyboard
  330. keymap_json['keymap'] = keymap
  331. return keymap_json