keymap.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. """Functions that help you work with QMK keymaps.
  2. """
  3. from pathlib import Path
  4. from milc import cli
  5. from qmk.keyboard import rules_mk
  6. import qmk.path
  7. # The `keymap.c` template to use when a keyboard doesn't have its own
  8. DEFAULT_KEYMAP_C = """#include QMK_KEYBOARD_H
  9. /* THIS FILE WAS GENERATED!
  10. *
  11. * This file was generated by qmk json2c. You may or may not want to
  12. * edit it directly.
  13. */
  14. const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
  15. __KEYMAP_GOES_HERE__
  16. };
  17. """
  18. def template(keyboard):
  19. """Returns the `keymap.c` template for a keyboard.
  20. If a template exists in `keyboards/<keyboard>/templates/keymap.c` that
  21. text will be used instead of `DEFAULT_KEYMAP_C`.
  22. Args:
  23. keyboard
  24. The keyboard to return a template for.
  25. """
  26. template_file = Path('keyboards/%s/templates/keymap.c' % keyboard)
  27. if template_file.exists():
  28. return template_file.read_text()
  29. return DEFAULT_KEYMAP_C
  30. def _strip_any(keycode):
  31. """Remove ANY() from a keycode.
  32. """
  33. if keycode.startswith('ANY(') and keycode.endswith(')'):
  34. keycode = keycode[4:-1]
  35. return keycode
  36. def is_keymap_dir(keymap):
  37. """Return True if Path object `keymap` has a keymap file inside.
  38. """
  39. for file in ('keymap.c', 'keymap.json'):
  40. if (keymap / file).is_file():
  41. return True
  42. def generate(keyboard, layout, layers):
  43. """Returns a keymap.c for the specified keyboard, layout, and layers.
  44. Args:
  45. keyboard
  46. The name of the keyboard
  47. layout
  48. The LAYOUT macro this keymap uses.
  49. layers
  50. An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode.
  51. """
  52. layer_txt = []
  53. for layer_num, layer in enumerate(layers):
  54. if layer_num != 0:
  55. layer_txt[-1] = layer_txt[-1] + ','
  56. layer = map(_strip_any, layer)
  57. layer_keys = ', '.join(layer)
  58. layer_txt.append('\t[%s] = %s(%s)' % (layer_num, layout, layer_keys))
  59. keymap = '\n'.join(layer_txt)
  60. keymap_c = template(keyboard)
  61. return keymap_c.replace('__KEYMAP_GOES_HERE__', keymap)
  62. def write(keyboard, keymap, layout, layers):
  63. """Generate the `keymap.c` and write it to disk.
  64. Returns the filename written to.
  65. Args:
  66. keyboard
  67. The name of the keyboard
  68. keymap
  69. The name of the keymap
  70. layout
  71. The LAYOUT macro this keymap uses.
  72. layers
  73. An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode.
  74. """
  75. keymap_c = generate(keyboard, layout, layers)
  76. keymap_file = qmk.path.keymap(keyboard) / keymap / 'keymap.c'
  77. keymap_file.parent.mkdir(parents=True, exist_ok=True)
  78. keymap_file.write_text(keymap_c)
  79. cli.log.info('Wrote keymap to {fg_cyan}%s', keymap_file)
  80. return keymap_file
  81. def locate_keymap(keyboard, keymap):
  82. """Returns the path to a keymap for a specific keyboard.
  83. """
  84. if not qmk.path.is_keyboard(keyboard):
  85. raise KeyError('Invalid keyboard: ' + repr(keyboard))
  86. # Check the keyboard folder first, last match wins
  87. checked_dirs = ''
  88. keymap_path = ''
  89. for dir in keyboard.split('/'):
  90. if checked_dirs:
  91. checked_dirs = '/'.join((checked_dirs, dir))
  92. else:
  93. checked_dirs = dir
  94. keymap_dir = Path('keyboards') / checked_dirs / 'keymaps'
  95. if (keymap_dir / keymap / 'keymap.c').exists():
  96. keymap_path = keymap_dir / keymap / 'keymap.c'
  97. if (keymap_dir / keymap / 'keymap.json').exists():
  98. keymap_path = keymap_dir / keymap / 'keymap.json'
  99. if keymap_path:
  100. return keymap_path
  101. # Check community layouts as a fallback
  102. rules = rules_mk(keyboard)
  103. if "LAYOUTS" in rules:
  104. for layout in rules["LAYOUTS"].split():
  105. community_layout = Path('layouts/community') / layout / keymap
  106. if community_layout.exists():
  107. if (community_layout / 'keymap.json').exists():
  108. return community_layout / 'keymap.json'
  109. if (community_layout / 'keymap.c').exists():
  110. return community_layout / 'keymap.c'
  111. def list_keymaps(keyboard):
  112. """ List the available keymaps for a keyboard.
  113. Args:
  114. keyboard: the keyboards full name with vendor and revision if necessary, example: clueboard/66/rev3
  115. Returns:
  116. a set with the names of the available keymaps
  117. """
  118. # parse all the rules.mk files for the keyboard
  119. rules = rules_mk(keyboard)
  120. names = set()
  121. if rules:
  122. # qmk_firmware/keyboards
  123. keyboards_dir = Path('keyboards')
  124. # path to the keyboard's directory
  125. kb_path = keyboards_dir / keyboard
  126. # walk up the directory tree until keyboards_dir
  127. # and collect all directories' name with keymap.c file in it
  128. while kb_path != keyboards_dir:
  129. keymaps_dir = kb_path / "keymaps"
  130. if keymaps_dir.exists():
  131. names = names.union([keymap.name for keymap in keymaps_dir.iterdir() if is_keymap_dir(keymap)])
  132. kb_path = kb_path.parent
  133. # if community layouts are supported, get them
  134. if "LAYOUTS" in rules:
  135. for layout in rules["LAYOUTS"].split():
  136. cl_path = Path('layouts/community') / layout
  137. if cl_path.exists():
  138. names = names.union([keymap.name for keymap in cl_path.iterdir() if is_keymap_dir(keymap)])
  139. return sorted(names)