kle2json.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. """Convert raw KLE to JSON
  2. """
  3. import json
  4. import os
  5. from pathlib import Path
  6. import requests
  7. from milc import cli
  8. from kle2xy import KLE2xy
  9. import qmk.path
  10. from qmk.converter import kle2qmk
  11. from qmk.decorators import automagic_keyboard
  12. from qmk.info import info_json
  13. from qmk.info_json_encoder import InfoJSONEncoder
  14. def fetch_json(url):
  15. """Gets the JSON from a url.
  16. """
  17. response = fetch_url(url)
  18. if response.status_code == 200:
  19. return response.json()
  20. print(f'ERROR: {url} returned {response.status_code}: {response.text}')
  21. return {}
  22. def fetch_url(url):
  23. """Fetch a URL.
  24. """
  25. response = requests.get(url, timeout=30)
  26. response.encoding='utf-8-sig'
  27. return response
  28. def fetch_gist(id):
  29. """Retrieve a gist from gist.github.com
  30. """
  31. url = f'https://api.github.com/gists/{id}'
  32. gist = fetch_json(url)
  33. for data in gist['files'].values():
  34. if data['filename'].endswith('kbd.json'):
  35. if data.get('truncated'):
  36. return fetch_url(data['raw_url']).text
  37. else:
  38. return data['content']
  39. return None
  40. def fetch_kle(id):
  41. """Fetch the kle data from a gist ID.
  42. """
  43. gist = fetch_gist(id)
  44. return gist[1:-1]
  45. @cli.argument('kle', arg_only=True, help='A file or KLE id to convert')
  46. @cli.argument('-l', '--layout', arg_only=True, default='LAYOUT', help='The LAYOUT name this KLE represents')
  47. @cli.argument('-kb', '--keyboard', arg_only=True, required=True, help='The folder name for the keyboard')
  48. @cli.argument('-km', '--keymap', arg_only=True, default='default', help='The name of the keymap to write (Default: default)')
  49. @cli.subcommand('Use a KLE layout to build info.json and a keymap', hidden=False if cli.config.user.developer else True)
  50. def kle2json(cli):
  51. """Convert a KLE layout to QMK's layout format.
  52. """
  53. file_path = Path(os.environ['ORIG_CWD'], cli.args.kle)
  54. # Find our KLE text
  55. if file_path.exists():
  56. raw_code = file_path.open().read()
  57. else:
  58. if cli.args.kle.startswith('http') and '#' in cli.args.kle:
  59. kle_path = cli.args.kle.split('#', 1)[1]
  60. if 'gists' not in kle_path:
  61. cli.log.error('Invalid KLE url: {fg_cyan}%s', cli.args.kle)
  62. return False
  63. else:
  64. raw_code = fetch_kle(kle_path.split('/')[-1])
  65. else:
  66. raw_code = fetch_kle(cli.args.kle)
  67. if not raw_code:
  68. cli.log.error('File {fg_cyan}%s{style_reset_all} was not found.', file_path)
  69. return False
  70. # Make sure the user supplied a keyboard
  71. if not cli.args.keyboard:
  72. cli.log.error('You must pass --keyboard or be in a keyboard directory!')
  73. cli.print_usage()
  74. return False
  75. # Check for an existing info.json
  76. if qmk.path.is_keyboard(cli.args.keyboard):
  77. kb_info_json = info_json(cli.args.keyboard)
  78. else:
  79. kb_info_json = {
  80. "keyboard_name": cli.args.keyboard,
  81. "maintainer": "",
  82. "features": {
  83. "console": True,
  84. "extrakey": True,
  85. "mousekey": True,
  86. "nkro": True
  87. },
  88. "matrix_pins": {
  89. "cols": [],
  90. "rows": [],
  91. },
  92. "usb": {
  93. "device_ver": "0x0001",
  94. "pid": '0x0000',
  95. "vid": '0x03A8',
  96. },
  97. "layouts": {},
  98. }
  99. # Build and merge in the new layout
  100. try:
  101. # Convert KLE raw to x/y coordinates (using kle2xy package from skullydazed)
  102. kle = KLE2xy(raw_code)
  103. except Exception as e:
  104. cli.log.error('Could not parse KLE raw data: %s', raw_code)
  105. cli.log.exception(e)
  106. return False
  107. if 'layouts' not in kb_info_json:
  108. kb_info_json['layouts'] = {}
  109. if cli.args.layout not in kb_info_json['layouts']:
  110. kb_info_json['layouts'][cli.args.layout] = {}
  111. kb_info_json['layouts'][cli.args.layout]['layout'] = kle2qmk(kle)
  112. # Write our info.json
  113. keyboard_dir = qmk.path.keyboard(cli.args.keyboard)
  114. keyboard_dir.mkdir(exist_ok=True, parents=True)
  115. info_json_file = keyboard_dir / 'info.json'
  116. json.dump(kb_info_json, info_json_file.open('w'), indent=4, separators=(', ', ': '), sort_keys=False, cls=InfoJSONEncoder)
  117. cli.log.info('Wrote file {fg_cyan}%s', info_json_file)
  118. # Generate and write a keymap
  119. keymap_path = keyboard_dir / 'keymaps' / cli.args.keymap
  120. keymap_file = keymap_path / 'keymap.json'
  121. if keymap_path.exists():
  122. cli.log.warning('{fg_cyan}%s{fg_reset} already exists, not generating a keymap.', keymap_path)
  123. else:
  124. keymap = [key.get('label', 'KC_NO') for key in kb_info_json['layouts'][cli.args.layout]['layout']]
  125. keymap_json = {
  126. 'version': 1,
  127. 'documentation': "This file is a QMK Keymap. You can compile it with `qmk compile` or import it at <https://config.qmk.fm>. It can also be used directly with QMK's source code.",
  128. 'author': '',
  129. 'keyboard': kb_info_json['keyboard_name'],
  130. 'keymap': cli.args.keymap,
  131. 'layout': cli.args.layout,
  132. 'layers': [
  133. keymap,
  134. ['KC_TRNS' for key in keymap],
  135. ],
  136. }
  137. keymap_path.mkdir(exist_ok=True, parents=True)
  138. json.dump(keymap_json, keymap_file.open('w'), indent=4, separators=(', ', ': '), sort_keys=False)
  139. cli.log.info('Wrote file %s', keymap_file)