import json
import sys
from pathlib import Path
from subprocess import DEVNULL
import argcomplete
from milc import cli
from pygments.lexers.c_cpp import CLexer
from pygments.token import Token
from pygments import lex
import qmk.path
from qmk.keyboard import find_keyboard_from_dir, rules_mk
from qmk.errors import CppError
DEFAULT_KEYMAP_C = """#include QMK_KEYBOARD_H
/* THIS FILE WAS GENERATED!
*
* This file was generated by qmk json2c. You may or may not want to
* edit it directly.
*/
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
__KEYMAP_GOES_HERE__
};
"""
def template_json(keyboard):
template_file = Path('keyboards/%s/templates/keymap.json' % keyboard)
template = {'keyboard': keyboard}
if template_file.exists():
template.update(json.load(template_file.open(encoding='utf-8')))
return template
def template_c(keyboard):
template_file = Path('keyboards/%s/templates/keymap.c' % keyboard)
if template_file.exists():
template = template_file.read_text(encoding='utf-8')
else:
template = DEFAULT_KEYMAP_C
return template
def _strip_any(keycode):
if keycode.startswith('ANY(') and keycode.endswith(')'):
keycode = keycode[4:-1]
return keycode
def find_keymap_from_dir():
relative_cwd = qmk.path.under_qmk_firmware()
if relative_cwd and len(relative_cwd.parts) > 1:
if relative_cwd.parts[0] == 'keyboards' and 'keymaps' in relative_cwd.parts:
current_path = Path('/'.join(relative_cwd.parts[1:]))
if 'keymaps' in current_path.parts and current_path.name != 'keymaps':
while current_path.parent.name != 'keymaps':
current_path = current_path.parent
return current_path.name, 'keymap_directory'
elif relative_cwd.parts[0] == 'layouts' and is_keymap_dir(relative_cwd):
return relative_cwd.name, 'layouts_directory'
elif relative_cwd.parts[0] == 'users':
return relative_cwd.parts[1], 'users_directory'
return None, None
def keymap_completer(prefix, action, parser, parsed_args):
try:
if parsed_args.keyboard:
return list_keymaps(parsed_args.keyboard)
keyboard = find_keyboard_from_dir()
if keyboard:
return list_keymaps(keyboard)
except Exception as e:
argcomplete.warn(f'Error: {e.__class__.__name__}: {str(e)}')
return []
return []
def is_keymap_dir(keymap, c=True, json=True, additional_files=None):
files = []
if c:
files.append('keymap.c')
if json:
files.append('keymap.json')
for file in files:
if (keymap / file).is_file():
if additional_files:
for file in additional_files:
if not (keymap / file).is_file():
return False
return True
def generate_json(keymap, keyboard, layout, layers):
new_keymap = template_json(keyboard)
new_keymap['keymap'] = keymap
new_keymap['layout'] = layout
new_keymap['layers'] = layers
return new_keymap
def generate_c(keyboard, layout, layers):
new_keymap = template_c(keyboard)
layer_txt = []
for layer_num, layer in enumerate(layers):
if layer_num != 0:
layer_txt[-1] = layer_txt[-1] + ','
layer = map(_strip_any, layer)
layer_keys = ', '.join(layer)
layer_txt.append('\t[%s] = %s(%s)' % (layer_num, layout, layer_keys))
keymap = '\n'.join(layer_txt)
new_keymap = new_keymap.replace('__KEYMAP_GOES_HERE__', keymap)
return new_keymap
def write_file(keymap_filename, keymap_content):
keymap_filename.parent.mkdir(parents=True, exist_ok=True)
keymap_filename.write_text(keymap_content)
cli.log.info('Wrote keymap to {fg_cyan}%s', keymap_filename)
return keymap_filename
def write_json(keyboard, keymap, layout, layers):
keymap_json = generate_json(keyboard, keymap, layout, layers)
keymap_content = json.dumps(keymap_json)
keymap_file = qmk.path.keymap(keyboard) / keymap / 'keymap.json'
return write_file(keymap_file, keymap_content)
def write(keyboard, keymap, layout, layers):
keymap_content = generate_c(keyboard, layout, layers)
keymap_file = qmk.path.keymap(keyboard) / keymap / 'keymap.c'
return write_file(keymap_file, keymap_content)
def locate_keymap(keyboard, keymap):
if not qmk.path.is_keyboard(keyboard):
raise KeyError('Invalid keyboard: ' + repr(keyboard))
checked_dirs = ''
keymap_path = ''
for dir in keyboard.split('/'):
if checked_dirs:
checked_dirs = '/'.join((checked_dirs, dir))
else:
checked_dirs = dir
keymap_dir = Path('keyboards') / checked_dirs / 'keymaps'
if (keymap_dir / keymap / 'keymap.c').exists():
keymap_path = keymap_dir / keymap / 'keymap.c'
if (keymap_dir / keymap / 'keymap.json').exists():
keymap_path = keymap_dir / keymap / 'keymap.json'
if keymap_path:
return keymap_path
rules = rules_mk(keyboard)
if "LAYOUTS" in rules:
for layout in rules["LAYOUTS"].split():
community_layout = Path('layouts/community') / layout / keymap
if community_layout.exists():
if (community_layout / 'keymap.json').exists():
return community_layout / 'keymap.json'
if (community_layout / 'keymap.c').exists():
return community_layout / 'keymap.c'
def list_keymaps(keyboard, c=True, json=True, additional_files=None, fullpath=False):
rules = rules_mk(keyboard)
names = set()
if rules:
keyboards_dir = Path('keyboards')
kb_path = keyboards_dir / keyboard
while kb_path != keyboards_dir:
keymaps_dir = kb_path / "keymaps"
if keymaps_dir.is_dir():
for keymap in keymaps_dir.iterdir():
if is_keymap_dir(keymap, c, json, additional_files):
keymap = keymap if fullpath else keymap.name
names.add(keymap)
kb_path = kb_path.parent
if "LAYOUTS" in rules:
for layout in rules["LAYOUTS"].split():
cl_path = Path('layouts/community') / layout
if cl_path.is_dir():
for keymap in cl_path.iterdir():
if is_keymap_dir(keymap, c, json, additional_files):
keymap = keymap if fullpath else keymap.name
names.add(keymap)
return sorted(names)
def _c_preprocess(path, stdin=DEVNULL):
cmd = ['cpp', str(path)] if path else ['cpp']
pre_processed_keymap = cli.run(cmd, stdin=stdin)
if 'fatal error' in pre_processed_keymap.stderr:
for line in pre_processed_keymap.stderr.split('\n'):
if 'fatal error' in line:
raise (CppError(line))
return pre_processed_keymap.stdout
def _get_layers(keymap):
layers = list()
opening_braces = '({['
closing_braces = ')}]'
keymap_certainty = brace_depth = 0
is_keymap = is_layer = is_adv_kc = False
layer = dict(name=False, layout=False, keycodes=list())
for line in lex(keymap, CLexer()):
if line[0] is Token.Name:
if is_keymap:
if not layer['name']:
if line[1].startswith('LAYOUT') or line[1].startswith('KEYMAP'):
layer['name'] = '0'
layer['layout'] = line[1]
else:
layer['name'] = line[1]
elif not layer['layout']:
layer['layout'] = line[1]
elif is_layer:
if line[1] == '_______':
kc = 'KC_TRNS'
elif line[1] == 'XXXXXXX':
kc = 'KC_NO'
else:
kc = line[1]
if is_adv_kc:
layer['keycodes'][-1] += kc
else:
layer['keycodes'].append(kc)
elif line[1] == 'PROGMEM' and keymap_certainty == 2:
keymap_certainty = 3
elif line[1] == 'keymaps' and keymap_certainty == 3:
keymap_certainty = 4
elif line[1] == 'MATRIX_ROWS' and keymap_certainty == 4:
keymap_certainty = 5
elif line[1] == 'MATRIX_COLS' and keymap_certainty == 5:
keymap_certainty = 6
elif line[0] is Token.Keyword:
if line[1] == 'const' and keymap_certainty == 0:
keymap_certainty = 1
elif line[0] is Token.Keyword.Type:
if line[1] == 'uint16_t' and keymap_certainty == 1:
keymap_certainty = 2
elif line[0] is Token.Punctuation:
if line[1] in opening_braces:
brace_depth += 1
if is_keymap:
if is_layer:
is_adv_kc = True
layer['keycodes'][-1] += line[1]
elif line[1] == '(' and brace_depth == 2:
is_layer = True
elif line[1] == '{' and keymap_certainty == 6:
is_keymap = True
elif line[1] in closing_braces:
brace_depth -= 1
if is_keymap:
if is_adv_kc:
layer['keycodes'][-1] += line[1]
if brace_depth == 2:
is_adv_kc = False
elif line[1] == ')' and brace_depth == 1:
is_layer = False
layers.append(layer)
layer = dict(name=False, layout=False, keycodes=list())
elif line[1] == '}' and brace_depth == 0:
is_keymap = False
keymap_certainty = 0
elif is_adv_kc:
layer['keycodes'][-1] += line[1]
elif line[0] is Token.Literal.Number.Integer and is_keymap and not is_adv_kc:
if not layer['name']:
layer['name'] = line[1]
else:
if is_adv_kc:
layer['keycodes'][-1] += line[1]
return layers
def parse_keymap_c(keymap_file, use_cpp=True):
if keymap_file == '-':
if use_cpp:
keymap_file = _c_preprocess(None, sys.stdin)
else:
keymap_file = sys.stdin.read()
else:
if use_cpp:
keymap_file = _c_preprocess(keymap_file)
else:
keymap_file = keymap_file.read_text(encoding='utf-8')
keymap = dict()
keymap['layers'] = _get_layers(keymap_file)
return keymap
def c2json(keyboard, keymap, keymap_file, use_cpp=True):
keymap_json = parse_keymap_c(keymap_file, use_cpp)
dirty_layers = keymap_json.pop('layers', None)
keymap_json['layers'] = list()
for layer in dirty_layers:
layer.pop('name')
layout = layer.pop('layout')
if not keymap_json.get('layout', False):
keymap_json['layout'] = layout
keymap_json['layers'].append(layer.pop('keycodes'))
keymap_json['keyboard'] = keyboard
keymap_json['keymap'] = keymap
return keymap_json