via2json.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. """Generate a keymap.c from a configurator export.
  2. """
  3. import json
  4. import re
  5. from milc import cli
  6. import qmk.keyboard
  7. import qmk.path
  8. from qmk.info import info_json
  9. from qmk.json_encoders import KeymapJSONEncoder
  10. from qmk.commands import parse_configurator_json, dump_lines
  11. from qmk.keymap import generate_json, list_keymaps, locate_keymap, parse_keymap_c
  12. def _find_via_layout_macro(keyboard):
  13. keymap_layout = None
  14. if 'via' in list_keymaps(keyboard):
  15. keymap_path = locate_keymap(keyboard, 'via')
  16. if keymap_path.suffix == '.json':
  17. keymap_layout = parse_configurator_json(keymap_path)['layout']
  18. else:
  19. keymap_layout = parse_keymap_c(keymap_path)['layers'][0]['layout']
  20. return keymap_layout
  21. def _convert_macros(via_macros):
  22. via_macros = list(filter(lambda f: bool(f), via_macros))
  23. if len(via_macros) == 0:
  24. return list()
  25. split_regex = re.compile(r'(}\,)|(\,{)')
  26. macro_group_regex = re.compile(r'({.+?})')
  27. macros = list()
  28. for via_macro in via_macros:
  29. # Split VIA macro to its elements
  30. macro = split_regex.split(via_macro)
  31. # Remove junk elements (None, '},' and ',{')
  32. macro = list(filter(lambda f: False if f in (None, '},', ',{') else True, macro))
  33. macro_data = list()
  34. for m in macro:
  35. if '{' in m or '}' in m:
  36. # Split macro groups
  37. macro_groups = macro_group_regex.findall(m)
  38. for macro_group in macro_groups:
  39. # Remove whitespaces and curly braces from around group
  40. macro_group = macro_group.strip(' {}')
  41. macro_action = 'tap'
  42. macro_keycodes = []
  43. if macro_group[0] == '+':
  44. macro_action = 'down'
  45. macro_keycodes.append(macro_group[1:])
  46. elif macro_group[0] == '-':
  47. macro_action = 'up'
  48. macro_keycodes.append(macro_group[1:])
  49. else:
  50. macro_keycodes.extend(macro_group.split(',') if ',' in macro_group else [macro_group])
  51. # Remove the KC prefixes
  52. macro_keycodes = list(map(lambda s: s.replace('KC_', ''), macro_keycodes))
  53. macro_data.append({"action": macro_action, "keycodes": macro_keycodes})
  54. else:
  55. # Found text
  56. macro_data.append(m)
  57. macros.append(macro_data)
  58. return macros
  59. def _fix_macro_keys(keymap_data):
  60. macro_no = re.compile(r'MACRO0?\(([0-9]{1,2})\)')
  61. for i in range(0, len(keymap_data)):
  62. for j in range(0, len(keymap_data[i])):
  63. kc = keymap_data[i][j]
  64. m = macro_no.match(kc)
  65. if m:
  66. keymap_data[i][j] = f'MC_{m.group(1)}'
  67. return keymap_data
  68. def _via_to_keymap(via_backup, keyboard_data, keymap_layout):
  69. # Check if passed LAYOUT is correct
  70. layout_data = keyboard_data['layouts'].get(keymap_layout)
  71. if not layout_data:
  72. cli.log.error(f'LAYOUT macro {keymap_layout} is not a valid one for keyboard {cli.args.keyboard}!')
  73. return None
  74. layout_data = layout_data['layout']
  75. sorting_hat = list()
  76. for index, data in enumerate(layout_data):
  77. sorting_hat.append([index, data['matrix']])
  78. sorting_hat.sort(key=lambda k: (k[1][0], k[1][1]))
  79. pos = 0
  80. for row_num in range(0, keyboard_data['matrix_size']['rows']):
  81. for col_num in range(0, keyboard_data['matrix_size']['cols']):
  82. if pos >= len(sorting_hat) or sorting_hat[pos][1][0] != row_num or sorting_hat[pos][1][1] != col_num:
  83. sorting_hat.insert(pos, [None, [row_num, col_num]])
  84. else:
  85. sorting_hat.append([None, [row_num, col_num]])
  86. pos += 1
  87. keymap_data = list()
  88. for layer in via_backup['layers']:
  89. pos = 0
  90. layer_data = list()
  91. for key in layer:
  92. if sorting_hat[pos][0] is not None:
  93. layer_data.append([sorting_hat[pos][0], key])
  94. pos += 1
  95. layer_data.sort()
  96. layer_data = [kc[1] for kc in layer_data]
  97. keymap_data.append(layer_data)
  98. return keymap_data
  99. @cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to')
  100. @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
  101. @cli.argument('filename', type=qmk.path.FileType('r'), arg_only=True, help='VIA Backup JSON file')
  102. @cli.argument('-kb', '--keyboard', type=qmk.keyboard.keyboard_folder, completer=qmk.keyboard.keyboard_completer, arg_only=True, required=True, help='The keyboard\'s name')
  103. @cli.argument('-km', '--keymap', arg_only=True, default='via2json', help='The keymap\'s name')
  104. @cli.argument('-l', '--layout', arg_only=True, help='The keymap\'s layout')
  105. @cli.subcommand('Convert a VIA backup json to keymap.json format.')
  106. def via2json(cli):
  107. """Convert a VIA backup json to keymap.json format.
  108. This command uses the `qmk.keymap` module to generate a keymap.json from a VIA backup json. The generated keymap is written to stdout, or to a file if -o is provided.
  109. """
  110. # Find appropriate layout macro
  111. keymap_layout = cli.args.layout if cli.args.layout else _find_via_layout_macro(cli.args.keyboard)
  112. if not keymap_layout:
  113. cli.log.error(f"Couldn't find LAYOUT macro for keyboard {cli.args.keyboard}. Please specify it with the '-l' argument.")
  114. return False
  115. # Load the VIA backup json
  116. with cli.args.filename.open('r') as fd:
  117. via_backup = json.load(fd)
  118. # Generate keyboard metadata
  119. keyboard_data = info_json(cli.args.keyboard)
  120. if not keyboard_data:
  121. cli.log.error(f'LAYOUT macro {keymap_layout} is not a valid one for keyboard {cli.args.keyboard}!')
  122. return False
  123. # Get keycode array
  124. keymap_data = _via_to_keymap(via_backup, keyboard_data, keymap_layout)
  125. if not keymap_data:
  126. cli.log.error(f'Could not extract valid keycode data from VIA backup matching keyboard {cli.args.keyboard}!')
  127. return False
  128. # Convert macros
  129. macro_data = list()
  130. if via_backup.get('macros'):
  131. macro_data = _convert_macros(via_backup['macros'])
  132. # Replace VIA macro keys with JSON keymap ones
  133. keymap_data = _fix_macro_keys(keymap_data)
  134. # Generate the keymap.json
  135. keymap_json = generate_json(cli.args.keymap, cli.args.keyboard, keymap_layout, keymap_data, macro_data)
  136. keymap_lines = [json.dumps(keymap_json, cls=KeymapJSONEncoder, sort_keys=True)]
  137. dump_lines(cli.args.output, keymap_lines, cli.args.quiet)