kle2json.py 5.4 KB

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