common.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. """This script handles the XAP protocol data files.
  2. """
  3. import os
  4. import hjson
  5. import jsonschema
  6. from pathlib import Path
  7. from typing import OrderedDict
  8. from jinja2 import Environment, FileSystemLoader, select_autoescape
  9. from qmk.constants import QMK_FIRMWARE
  10. from qmk.json_schema import json_load, validate
  11. from qmk.decorators import lru_cache
  12. from qmk.keymap import locate_keymap
  13. from qmk.path import keyboard
  14. XAP_SPEC = 'xap.hjson'
  15. def _get_jinja2_env(data_templates_xap_subdir: str):
  16. templates_dir = os.path.join(QMK_FIRMWARE, 'data', 'templates', 'xap', data_templates_xap_subdir)
  17. j2 = Environment(loader=FileSystemLoader(templates_dir), autoescape=select_autoescape())
  18. return j2
  19. def render_xap_output(data_templates_xap_subdir, file_to_render, defs):
  20. j2 = _get_jinja2_env(data_templates_xap_subdir)
  21. return j2.get_template(file_to_render).render(xap=defs, xap_str=hjson.dumps(defs))
  22. def _find_kb_spec(kb):
  23. base_path = Path('keyboards')
  24. keyboard_parent = keyboard(kb)
  25. for _ in range(5):
  26. if keyboard_parent == base_path:
  27. break
  28. spec = keyboard_parent / XAP_SPEC
  29. if spec.exists():
  30. return spec
  31. keyboard_parent = keyboard_parent.parent
  32. # Just return something we know doesn't exist
  33. return keyboard(kb) / XAP_SPEC
  34. def _find_km_spec(kb, km):
  35. return locate_keymap(kb, km).parent / XAP_SPEC
  36. def _merge_ordered_dicts(dicts):
  37. """Merges nested OrderedDict objects resulting from reading a hjson file.
  38. Later input dicts overrides earlier dicts for plain values.
  39. Arrays will be appended. If the first entry of an array is "!reset!", the contents of the array will be cleared and replaced with RHS.
  40. Dictionaries will be recursively merged. If any entry is "!reset!", the contents of the dictionary will be cleared and replaced with RHS.
  41. """
  42. result = OrderedDict()
  43. def add_entry(target, k, v):
  44. if k in target and isinstance(v, (OrderedDict, dict)):
  45. if "!reset!" in v:
  46. target[k] = v
  47. else:
  48. target[k] = _merge_ordered_dicts([target[k], v])
  49. if "!reset!" in target[k]:
  50. del target[k]["!reset!"]
  51. elif k in target and isinstance(v, list):
  52. if v[0] == '!reset!':
  53. target[k] = v[1:]
  54. else:
  55. target[k] = target[k] + v
  56. else:
  57. target[k] = v
  58. for d in dicts:
  59. for (k, v) in d.items():
  60. add_entry(result, k, v)
  61. return result
  62. def get_xap_definition_files():
  63. """Get the sorted list of XAP definition files, from <QMK>/data/xap.
  64. """
  65. xap_defs = QMK_FIRMWARE / "data" / "xap"
  66. return list(sorted(xap_defs.glob('**/xap_*.hjson')))
  67. def update_xap_definitions(original, new):
  68. """Creates a new XAP definition object based on an original and the new supplied object.
  69. Both inputs must be of type OrderedDict.
  70. Later input dicts overrides earlier dicts for plain values.
  71. Arrays will be appended. If the first entry of an array is "!reset!", the contents of the array will be cleared and replaced with RHS.
  72. Dictionaries will be recursively merged. If any entry is "!reset!", the contents of the dictionary will be cleared and replaced with RHS.
  73. """
  74. if original is None:
  75. original = OrderedDict()
  76. return _merge_ordered_dicts([original, new])
  77. @lru_cache(timeout=5)
  78. def get_xap_defs(version):
  79. """Gets the required version of the XAP definitions.
  80. """
  81. files = get_xap_definition_files()
  82. # Slice off anything newer than specified version
  83. if version != 'latest':
  84. index = [idx for idx, s in enumerate(files) if version in str(s)][0]
  85. files = files[:(index + 1)]
  86. definitions = [hjson.load(file.open(encoding='utf-8')) for file in files]
  87. return _merge_ordered_dicts(definitions)
  88. def latest_xap_defs():
  89. """Gets the latest version of the XAP definitions.
  90. """
  91. return get_xap_defs('latest')
  92. def merge_xap_defs(kb, km):
  93. """Gets the latest version of the XAP definitions and merges in optional keyboard/keymap specs
  94. """
  95. definitions = [get_xap_defs('latest')]
  96. kb_xap = _find_kb_spec(kb)
  97. if kb_xap.exists():
  98. definitions.append({'routes': {'0x02': hjson.load(kb_xap.open(encoding='utf-8'))}})
  99. km_xap = _find_km_spec(kb, km)
  100. if km_xap.exists():
  101. definitions.append({'routes': {'0x03': hjson.load(km_xap.open(encoding='utf-8'))}})
  102. defs = _merge_ordered_dicts(definitions)
  103. try:
  104. validate(defs, 'qmk.xap.v1')
  105. except jsonschema.ValidationError as e:
  106. print(f'Invalid XAP spec: {e.message}')
  107. exit(1)
  108. return defs
  109. @lru_cache(timeout=5)
  110. def get_xap_keycodes(xap_version):
  111. """Gets keycode data for the required version of the XAP definitions.
  112. """
  113. defs = get_xap_defs(xap_version)
  114. # Load DD keycodes for the dependency
  115. keycode_version = defs['uses']['keycodes']
  116. spec = json_load(Path(f'data/constants/keycodes_{keycode_version}.json'))
  117. # Transform into something more usable - { raw_value : first alias || keycode }
  118. return {int(k, 16): v.get('aliases', [v.get('key')])[0] for k, v in spec['keycodes'].items()}
  119. def route_conditions(route_stack):
  120. """Handles building the C preprocessor conditional based on the current route.
  121. """
  122. conditions = []
  123. for route in route_stack:
  124. if 'enable_if_preprocessor' in route:
  125. conditions.append(route['enable_if_preprocessor'])
  126. if len(conditions) == 0:
  127. return None
  128. return "(" + ' && '.join([f'({c})' for c in conditions]) + ")"