keymap.py 14 KB

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