aboutsummaryrefslogtreecommitdiff
path: root/lib/python
diff options
context:
space:
mode:
Diffstat (limited to 'lib/python')
-rw-r--r--lib/python/qmk/c_parse.py41
-rw-r--r--lib/python/qmk/cli/__init__.py23
-rw-r--r--lib/python/qmk/cli/c2json.py3
-rw-r--r--lib/python/qmk/cli/chibios/confmigrate.py17
-rw-r--r--lib/python/qmk/cli/generate/__init__.py4
-rwxr-xr-xlib/python/qmk/cli/generate/api.py14
-rwxr-xr-xlib/python/qmk/cli/generate/config_h.py152
-rwxr-xr-xlib/python/qmk/cli/generate/info_json.py65
-rwxr-xr-xlib/python/qmk/cli/generate/layouts.py102
-rwxr-xr-xlib/python/qmk/cli/generate/rules_mk.py91
-rwxr-xr-xlib/python/qmk/cli/info.py14
-rw-r--r--lib/python/qmk/cli/json/__init__.py5
-rwxr-xr-xlib/python/qmk/cli/json/keymap.py16
-rwxr-xr-xlib/python/qmk/cli/kle2json.py53
-rw-r--r--lib/python/qmk/constants.py15
-rw-r--r--lib/python/qmk/info.py487
-rwxr-xr-xlib/python/qmk/info_json_encoder.py96
-rw-r--r--lib/python/qmk/keymap.py6
-rw-r--r--lib/python/qmk/os_helpers/linux/__init__.py3
-rw-r--r--lib/python/qmk/tests/test_cli_commands.py31
20 files changed, 1050 insertions, 188 deletions
diff --git a/lib/python/qmk/c_parse.py b/lib/python/qmk/c_parse.py
index e41e271a4..d4f39c883 100644
--- a/lib/python/qmk/c_parse.py
+++ b/lib/python/qmk/c_parse.py
@@ -1,12 +1,27 @@
1"""Functions for working with config.h files. 1"""Functions for working with config.h files.
2""" 2"""
3from pathlib import Path 3from pathlib import Path
4import re
4 5
5from milc import cli 6from milc import cli
6 7
7from qmk.comment_remover import comment_remover 8from qmk.comment_remover import comment_remover
8 9
9default_key_entry = {'x': -1, 'y': 0, 'w': 1} 10default_key_entry = {'x': -1, 'y': 0, 'w': 1}
11single_comment_regex = re.compile(r' */[/*].*$')
12multi_comment_regex = re.compile(r'/\*(.|\n)*?\*/', re.MULTILINE)
13
14
15def strip_line_comment(string):
16 """Removes comments from a single line string.
17 """
18 return single_comment_regex.sub('', string)
19
20
21def strip_multiline_comment(string):
22 """Removes comments from a single line string.
23 """
24 return multi_comment_regex.sub('', string)
10 25
11 26
12def c_source_files(dir_names): 27def c_source_files(dir_names):
@@ -31,7 +46,7 @@ def find_layouts(file):
31 parsed_layouts = {} 46 parsed_layouts = {}
32 47
33 # Search the file for LAYOUT macros and aliases 48 # Search the file for LAYOUT macros and aliases
34 file_contents = file.read_text() 49 file_contents = file.read_text(encoding='utf-8')
35 file_contents = comment_remover(file_contents) 50 file_contents = comment_remover(file_contents)
36 file_contents = file_contents.replace('\\\n', '') 51 file_contents = file_contents.replace('\\\n', '')
37 52
@@ -52,8 +67,11 @@ def find_layouts(file):
52 layout = layout.strip() 67 layout = layout.strip()
53 parsed_layout = [_default_key(key) for key in layout.split(',')] 68 parsed_layout = [_default_key(key) for key in layout.split(',')]
54 69
55 for key in parsed_layout: 70 for i, key in enumerate(parsed_layout):
56 key['matrix'] = matrix_locations.get(key['label']) 71 if 'label' not in key:
72 cli.log.error('Invalid LAYOUT macro in %s: Empty parameter name in macro %s at pos %s.', file, macro_name, i)
73 elif key['label'] in matrix_locations:
74 key['matrix'] = matrix_locations[key['label']]
57 75
58 parsed_layouts[macro_name] = { 76 parsed_layouts[macro_name] = {
59 'key_count': len(parsed_layout), 77 'key_count': len(parsed_layout),
@@ -69,12 +87,7 @@ def find_layouts(file):
69 except ValueError: 87 except ValueError:
70 continue 88 continue
71 89
72 # Populate our aliases 90 return parsed_layouts, aliases
73 for alias, text in aliases.items():
74 if text in parsed_layouts and 'KEYMAP' not in alias:
75 parsed_layouts[alias] = parsed_layouts[text]
76
77 return parsed_layouts
78 91
79 92
80def parse_config_h_file(config_h_file, config_h=None): 93def parse_config_h_file(config_h_file, config_h=None):
@@ -86,14 +99,12 @@ def parse_config_h_file(config_h_file, config_h=None):
86 config_h_file = Path(config_h_file) 99 config_h_file = Path(config_h_file)
87 100
88 if config_h_file.exists(): 101 if config_h_file.exists():
89 config_h_text = config_h_file.read_text() 102 config_h_text = config_h_file.read_text(encoding='utf-8')
90 config_h_text = config_h_text.replace('\\\n', '') 103 config_h_text = config_h_text.replace('\\\n', '')
104 config_h_text = strip_multiline_comment(config_h_text)
91 105
92 for linenum, line in enumerate(config_h_text.split('\n')): 106 for linenum, line in enumerate(config_h_text.split('\n')):
93 line = line.strip() 107 line = strip_line_comment(line).strip()
94
95 if '//' in line:
96 line = line[:line.index('//')].strip()
97 108
98 if not line: 109 if not line:
99 continue 110 continue
@@ -156,6 +167,6 @@ def _parse_matrix_locations(matrix, file, macro_name):
156 row = row.replace('{', '').replace('}', '') 167 row = row.replace('{', '').replace('}', '')
157 for col_num, identifier in enumerate(row.split(',')): 168 for col_num, identifier in enumerate(row.split(',')):
158 if identifier != 'KC_NO': 169 if identifier != 'KC_NO':
159 matrix_locations[identifier] = (row_num, col_num) 170 matrix_locations[identifier] = [row_num, col_num]
160 171
161 return matrix_locations 172 return matrix_locations
diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py
index 372c40921..a5f1f4767 100644
--- a/lib/python/qmk/cli/__init__.py
+++ b/lib/python/qmk/cli/__init__.py
@@ -19,7 +19,6 @@ from . import flash
19from . import generate 19from . import generate
20from . import hello 20from . import hello
21from . import info 21from . import info
22from . import json
23from . import json2c 22from . import json2c
24from . import lint 23from . import lint
25from . import list 24from . import list
@@ -28,6 +27,24 @@ from . import new
28from . import pyformat 27from . import pyformat
29from . import pytest 28from . import pytest
30 29
31if sys.version_info[0] != 3 or sys.version_info[1] < 6: 30# Supported version information
32 cli.log.error('Your Python is too old! Please upgrade to Python 3.6 or later.') 31#
32# Based on the OSes we support these are the minimum python version available by default.
33# Last update: 2021 Jan 02
34#
35# Arch: 3.9
36# Debian: 3.7
37# Fedora 31: 3.7
38# Fedora 32: 3.8
39# Fedora 33: 3.9
40# FreeBSD: 3.7
41# Gentoo: 3.7
42# macOS: 3.9 (from homebrew)
43# msys2: 3.8
44# Slackware: 3.7
45# solus: 3.7
46# void: 3.9
47
48if sys.version_info[0] != 3 or sys.version_info[1] < 7:
49 cli.log.error('Your Python is too old! Please upgrade to Python 3.7 or later.')
33 exit(127) 50 exit(127)
diff --git a/lib/python/qmk/cli/c2json.py b/lib/python/qmk/cli/c2json.py
index 2b3bb774f..8f9d8dc38 100644
--- a/lib/python/qmk/cli/c2json.py
+++ b/lib/python/qmk/cli/c2json.py
@@ -6,6 +6,7 @@ from milc import cli
6 6
7import qmk.keymap 7import qmk.keymap
8import qmk.path 8import qmk.path
9from qmk.info_json_encoder import InfoJSONEncoder
9 10
10 11
11@cli.argument('--no-cpp', arg_only=True, action='store_false', help='Do not use \'cpp\' on keymap.c') 12@cli.argument('--no-cpp', arg_only=True, action='store_false', help='Do not use \'cpp\' on keymap.c')
@@ -47,7 +48,7 @@ def c2json(cli):
47 cli.args.output.parent.mkdir(parents=True, exist_ok=True) 48 cli.args.output.parent.mkdir(parents=True, exist_ok=True)
48 if cli.args.output.exists(): 49 if cli.args.output.exists():
49 cli.args.output.replace(cli.args.output.name + '.bak') 50 cli.args.output.replace(cli.args.output.name + '.bak')
50 cli.args.output.write_text(json.dumps(keymap_json)) 51 cli.args.output.write_text(json.dumps(keymap_json, cls=InfoJSONEncoder))
51 52
52 if not cli.args.quiet: 53 if not cli.args.quiet:
53 cli.log.info('Wrote keymap to %s.', cli.args.output) 54 cli.log.info('Wrote keymap to %s.', cli.args.output)
diff --git a/lib/python/qmk/cli/chibios/confmigrate.py b/lib/python/qmk/cli/chibios/confmigrate.py
index b9cfda961..89995931a 100644
--- a/lib/python/qmk/cli/chibios/confmigrate.py
+++ b/lib/python/qmk/cli/chibios/confmigrate.py
@@ -32,7 +32,7 @@ file_header = """\
32 32
33/* 33/*
34 * This file was auto-generated by: 34 * This file was auto-generated by:
35 * `qmk chibios-confupdate -i {0} -r {1}` 35 * `qmk chibios-confmigrate -i {0} -r {1}`
36 */ 36 */
37 37
38#pragma once 38#pragma once
@@ -40,7 +40,7 @@ file_header = """\
40 40
41 41
42def collect_defines(filepath): 42def collect_defines(filepath):
43 with open(filepath, 'r') as f: 43 with open(filepath, 'r', encoding='utf-8') as f:
44 content = f.read() 44 content = f.read()
45 define_search = re.compile(r'(?m)^#\s*define\s+(?:.*\\\r?\n)*.*$', re.MULTILINE) 45 define_search = re.compile(r'(?m)^#\s*define\s+(?:.*\\\r?\n)*.*$', re.MULTILINE)
46 value_search = re.compile(r'^#\s*define\s+(?P<name>[a-zA-Z0-9_]+(\([^\)]*\))?)\s*(?P<value>.*)', re.DOTALL) 46 value_search = re.compile(r'^#\s*define\s+(?P<name>[a-zA-Z0-9_]+(\([^\)]*\))?)\s*(?P<value>.*)', re.DOTALL)
@@ -111,6 +111,7 @@ def migrate_mcuconf_h(to_override, outfile):
111@cli.argument('-r', '--reference', type=normpath, arg_only=True, help='Specify the reference file to compare against') 111@cli.argument('-r', '--reference', type=normpath, arg_only=True, help='Specify the reference file to compare against')
112@cli.argument('-o', '--overwrite', arg_only=True, action='store_true', help='Overwrites the input file during migration.') 112@cli.argument('-o', '--overwrite', arg_only=True, action='store_true', help='Overwrites the input file during migration.')
113@cli.argument('-d', '--delete', arg_only=True, action='store_true', help='If the file has no overrides, migration will delete the input file.') 113@cli.argument('-d', '--delete', arg_only=True, action='store_true', help='If the file has no overrides, migration will delete the input file.')
114@cli.argument('-f', '--force', arg_only=True, action='store_true', help='Re-migrates an already migrated file, even if it doesn\'t detect a full ChibiOS config.')
114@cli.subcommand('Generates a migrated ChibiOS configuration file, as a result of comparing the input against a reference') 115@cli.subcommand('Generates a migrated ChibiOS configuration file, as a result of comparing the input against a reference')
115def chibios_confmigrate(cli): 116def chibios_confmigrate(cli):
116 """Generates a usable ChibiOS replacement configuration file, based on a fully-defined conf and a reference config. 117 """Generates a usable ChibiOS replacement configuration file, based on a fully-defined conf and a reference config.
@@ -142,20 +143,20 @@ def chibios_confmigrate(cli):
142 143
143 eprint('--------------------------------------') 144 eprint('--------------------------------------')
144 145
145 if "CHCONF_H" in input_defs["dict"] or "_CHCONF_H_" in input_defs["dict"]: 146 if cli.args.input.name == "chconf.h" and ("CHCONF_H" in input_defs["dict"] or "_CHCONF_H_" in input_defs["dict"] or cli.args.force):
146 migrate_chconf_h(to_override, outfile=sys.stdout) 147 migrate_chconf_h(to_override, outfile=sys.stdout)
147 if cli.args.overwrite: 148 if cli.args.overwrite:
148 with open(cli.args.input, "w") as out_file: 149 with open(cli.args.input, "w", encoding='utf-8') as out_file:
149 migrate_chconf_h(to_override, outfile=out_file) 150 migrate_chconf_h(to_override, outfile=out_file)
150 151
151 elif "HALCONF_H" in input_defs["dict"] or "_HALCONF_H_" in input_defs["dict"]: 152 elif cli.args.input.name == "halconf.h" and ("HALCONF_H" in input_defs["dict"] or "_HALCONF_H_" in input_defs["dict"] or cli.args.force):
152 migrate_halconf_h(to_override, outfile=sys.stdout) 153 migrate_halconf_h(to_override, outfile=sys.stdout)
153 if cli.args.overwrite: 154 if cli.args.overwrite:
154 with open(cli.args.input, "w") as out_file: 155 with open(cli.args.input, "w", encoding='utf-8') as out_file:
155 migrate_halconf_h(to_override, outfile=out_file) 156 migrate_halconf_h(to_override, outfile=out_file)
156 157
157 elif "MCUCONF_H" in input_defs["dict"] or "_MCUCONF_H_" in input_defs["dict"]: 158 elif cli.args.input.name == "mcuconf.h" and ("MCUCONF_H" in input_defs["dict"] or "_MCUCONF_H_" in input_defs["dict"] or cli.args.force):
158 migrate_mcuconf_h(to_override, outfile=sys.stdout) 159 migrate_mcuconf_h(to_override, outfile=sys.stdout)
159 if cli.args.overwrite: 160 if cli.args.overwrite:
160 with open(cli.args.input, "w") as out_file: 161 with open(cli.args.input, "w", encoding='utf-8') as out_file:
161 migrate_mcuconf_h(to_override, outfile=out_file) 162 migrate_mcuconf_h(to_override, outfile=out_file)
diff --git a/lib/python/qmk/cli/generate/__init__.py b/lib/python/qmk/cli/generate/__init__.py
index f9585bfb5..bd75b044c 100644
--- a/lib/python/qmk/cli/generate/__init__.py
+++ b/lib/python/qmk/cli/generate/__init__.py
@@ -1,3 +1,7 @@
1from . import api 1from . import api
2from . import config_h
2from . import docs 3from . import docs
4from . import info_json
5from . import layouts
3from . import rgb_breathe_table 6from . import rgb_breathe_table
7from . import rules_mk
diff --git a/lib/python/qmk/cli/generate/api.py b/lib/python/qmk/cli/generate/api.py
index 66db37cb5..6d111f244 100755
--- a/lib/python/qmk/cli/generate/api.py
+++ b/lib/python/qmk/cli/generate/api.py
@@ -8,6 +8,7 @@ from milc import cli
8 8
9from qmk.datetime import current_datetime 9from qmk.datetime import current_datetime
10from qmk.info import info_json 10from qmk.info import info_json
11from qmk.info_json_encoder import InfoJSONEncoder
11from qmk.keyboard import list_keyboards 12from qmk.keyboard import list_keyboards
12 13
13 14
@@ -44,15 +45,16 @@ def generate_api(cli):
44 if 'usb' in kb_all['keyboards'][keyboard_name]: 45 if 'usb' in kb_all['keyboards'][keyboard_name]:
45 usb = kb_all['keyboards'][keyboard_name]['usb'] 46 usb = kb_all['keyboards'][keyboard_name]['usb']
46 47
47 if usb['vid'] not in usb_list['devices']: 48 if 'vid' in usb and usb['vid'] not in usb_list['devices']:
48 usb_list['devices'][usb['vid']] = {} 49 usb_list['devices'][usb['vid']] = {}
49 50
50 if usb['pid'] not in usb_list['devices'][usb['vid']]: 51 if 'pid' in usb and usb['pid'] not in usb_list['devices'][usb['vid']]:
51 usb_list['devices'][usb['vid']][usb['pid']] = {} 52 usb_list['devices'][usb['vid']][usb['pid']] = {}
52 53
53 usb_list['devices'][usb['vid']][usb['pid']][keyboard_name] = usb 54 if 'vid' in usb and 'pid' in usb:
55 usb_list['devices'][usb['vid']][usb['pid']][keyboard_name] = usb
54 56
55 # Write the global JSON files 57 # Write the global JSON files
56 keyboard_list.write_text(json.dumps({'last_updated': current_datetime(), 'keyboards': sorted(kb_all['keyboards'])})) 58 keyboard_list.write_text(json.dumps({'last_updated': current_datetime(), 'keyboards': sorted(kb_all['keyboards'])}, cls=InfoJSONEncoder))
57 keyboard_all.write_text(json.dumps(kb_all)) 59 keyboard_all.write_text(json.dumps(kb_all, cls=InfoJSONEncoder))
58 usb_file.write_text(json.dumps(usb_list)) 60 usb_file.write_text(json.dumps(usb_list, cls=InfoJSONEncoder))
diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py
new file mode 100755
index 000000000..7ddad745d
--- /dev/null
+++ b/lib/python/qmk/cli/generate/config_h.py
@@ -0,0 +1,152 @@
1"""Used by the make system to generate info_config.h from info.json.
2"""
3from pathlib import Path
4
5from dotty_dict import dotty
6from milc import cli
7
8from qmk.decorators import automagic_keyboard, automagic_keymap
9from qmk.info import _json_load, info_json
10from qmk.path import is_keyboard, normpath
11
12
13def direct_pins(direct_pins):
14 """Return the config.h lines that set the direct pins.
15 """
16 rows = []
17
18 for row in direct_pins:
19 cols = ','.join(map(str, [col or 'NO_PIN' for col in row]))
20 rows.append('{' + cols + '}')
21
22 col_count = len(direct_pins[0])
23 row_count = len(direct_pins)
24
25 return """
26#ifndef MATRIX_COLS
27# define MATRIX_COLS %s
28#endif // MATRIX_COLS
29
30#ifndef MATRIX_ROWS
31# define MATRIX_ROWS %s
32#endif // MATRIX_ROWS
33
34#ifndef DIRECT_PINS
35# define DIRECT_PINS {%s}
36#endif // DIRECT_PINS
37""" % (col_count, row_count, ','.join(rows))
38
39
40def pin_array(define, pins):
41 """Return the config.h lines that set a pin array.
42 """
43 pin_num = len(pins)
44 pin_array = ', '.join(map(str, [pin or 'NO_PIN' for pin in pins]))
45
46 return f"""
47#ifndef {define}S
48# define {define}S {pin_num}
49#endif // {define}S
50
51#ifndef {define}_PINS
52# define {define}_PINS {{ {pin_array} }}
53#endif // {define}_PINS
54"""
55
56
57def matrix_pins(matrix_pins):
58 """Add the matrix config to the config.h.
59 """
60 pins = []
61
62 if 'direct' in matrix_pins:
63 pins.append(direct_pins(matrix_pins['direct']))
64
65 if 'cols' in matrix_pins:
66 pins.append(pin_array('MATRIX_COL', matrix_pins['cols']))
67
68 if 'rows' in matrix_pins:
69 pins.append(pin_array('MATRIX_ROW', matrix_pins['rows']))
70
71 return '\n'.join(pins)
72
73
74@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
75@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
76@cli.argument('-kb', '--keyboard', help='Keyboard to generate config.h for.')
77@cli.subcommand('Used by the make system to generate info_config.h from info.json', hidden=True)
78@automagic_keyboard
79@automagic_keymap
80def generate_config_h(cli):
81 """Generates the info_config.h file.
82 """
83 # Determine our keyboard(s)
84 if not cli.config.generate_config_h.keyboard:
85 cli.log.error('Missing paramater: --keyboard')
86 cli.subcommands['info'].print_help()
87 return False
88
89 if not is_keyboard(cli.config.generate_config_h.keyboard):
90 cli.log.error('Invalid keyboard: "%s"', cli.config.generate_config_h.keyboard)
91 return False
92
93 # Build the info_config.h file.
94 kb_info_json = dotty(info_json(cli.config.generate_config_h.keyboard))
95 info_config_map = _json_load(Path('data/mappings/info_config.json'))
96
97 config_h_lines = ['/* This file was generated by `qmk generate-config-h`. Do not edit or copy.' ' */', '', '#pragma once']
98
99 # Iterate through the info_config map to generate basic things
100 for config_key, info_dict in info_config_map.items():
101 info_key = info_dict['info_key']
102 key_type = info_dict.get('value_type', 'str')
103 to_config = info_dict.get('to_config', True)
104
105 if not to_config:
106 continue
107
108 try:
109 config_value = kb_info_json[info_key]
110 except KeyError:
111 continue
112
113 if key_type.startswith('array'):
114 config_h_lines.append('')
115 config_h_lines.append(f'#ifndef {config_key}')
116 config_h_lines.append(f'# define {config_key} {{ {", ".join(map(str, config_value))} }}')
117 config_h_lines.append(f'#endif // {config_key}')
118 elif key_type == 'bool':
119 if config_value:
120 config_h_lines.append('')
121 config_h_lines.append(f'#ifndef {config_key}')
122 config_h_lines.append(f'# define {config_key}')
123 config_h_lines.append(f'#endif // {config_key}')
124 elif key_type == 'mapping':
125 for key, value in config_value.items():
126 config_h_lines.append('')
127 config_h_lines.append(f'#ifndef {key}')
128 config_h_lines.append(f'# define {key} {value}')
129 config_h_lines.append(f'#endif // {key}')
130 else:
131 config_h_lines.append('')
132 config_h_lines.append(f'#ifndef {config_key}')
133 config_h_lines.append(f'# define {config_key} {config_value}')
134 config_h_lines.append(f'#endif // {config_key}')
135
136 if 'matrix_pins' in kb_info_json:
137 config_h_lines.append(matrix_pins(kb_info_json['matrix_pins']))
138
139 # Show the results
140 config_h = '\n'.join(config_h_lines)
141
142 if cli.args.output:
143 cli.args.output.parent.mkdir(parents=True, exist_ok=True)
144 if cli.args.output.exists():
145 cli.args.output.replace(cli.args.output.name + '.bak')
146 cli.args.output.write_text(config_h)
147
148 if not cli.args.quiet:
149 cli.log.info('Wrote info_config.h to %s.', cli.args.output)
150
151 else:
152 print(config_h)
diff --git a/lib/python/qmk/cli/generate/info_json.py b/lib/python/qmk/cli/generate/info_json.py
new file mode 100755
index 000000000..f3fc54ddc
--- /dev/null
+++ b/lib/python/qmk/cli/generate/info_json.py
@@ -0,0 +1,65 @@
1"""Keyboard information script.
2
3Compile an info.json for a particular keyboard and pretty-print it.
4"""
5import json
6
7from jsonschema import Draft7Validator, validators
8from milc import cli
9
10from qmk.decorators import automagic_keyboard, automagic_keymap
11from qmk.info import info_json, _jsonschema
12from qmk.info_json_encoder import InfoJSONEncoder
13from qmk.path import is_keyboard
14
15
16def pruning_validator(validator_class):
17 """Extends Draft7Validator to remove properties that aren't specified in the schema.
18 """
19 validate_properties = validator_class.VALIDATORS["properties"]
20
21 def remove_additional_properties(validator, properties, instance, schema):
22 for prop in list(instance.keys()):
23 if prop not in properties:
24 del instance[prop]
25
26 for error in validate_properties(validator, properties, instance, schema):
27 yield error
28
29 return validators.extend(validator_class, {"properties": remove_additional_properties})
30
31
32def strip_info_json(kb_info_json):
33 """Remove the API-only properties from the info.json.
34 """
35 pruning_draft_7_validator = pruning_validator(Draft7Validator)
36 schema = _jsonschema('keyboard')
37 validator = pruning_draft_7_validator(schema).validate
38
39 return validator(kb_info_json)
40
41
42@cli.argument('-kb', '--keyboard', help='Keyboard to show info for.')
43@cli.argument('-km', '--keymap', help='Show the layers for a JSON keymap too.')
44@cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True)
45@automagic_keyboard
46@automagic_keymap
47def generate_info_json(cli):
48 """Generate an info.json file for a keyboard
49 """
50 # Determine our keyboard(s)
51 if not cli.config.generate_info_json.keyboard:
52 cli.log.error('Missing parameter: --keyboard')
53 cli.subcommands['info'].print_help()
54 return False
55
56 if not is_keyboard(cli.config.generate_info_json.keyboard):
57 cli.log.error('Invalid keyboard: "%s"', cli.config.generate_info_json.keyboard)
58 return False
59
60 # Build the info.json file
61 kb_info_json = info_json(cli.config.generate_info_json.keyboard)
62 strip_info_json(kb_info_json)
63
64 # Display the results
65 print(json.dumps(kb_info_json, indent=2, cls=InfoJSONEncoder))
diff --git a/lib/python/qmk/cli/generate/layouts.py b/lib/python/qmk/cli/generate/layouts.py
new file mode 100755
index 000000000..15b289522
--- /dev/null
+++ b/lib/python/qmk/cli/generate/layouts.py
@@ -0,0 +1,102 @@
1"""Used by the make system to generate layouts.h from info.json.
2"""
3from milc import cli
4
5from qmk.constants import COL_LETTERS, ROW_LETTERS
6from qmk.decorators import automagic_keyboard, automagic_keymap
7from qmk.info import info_json
8from qmk.path import is_keyboard, normpath
9
10usb_properties = {
11 'vid': 'VENDOR_ID',
12 'pid': 'PRODUCT_ID',
13 'device_ver': 'DEVICE_VER',
14}
15
16
17@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
18@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
19@cli.argument('-kb', '--keyboard', help='Keyboard to generate config.h for.')
20@cli.subcommand('Used by the make system to generate layouts.h from info.json', hidden=True)
21@automagic_keyboard
22@automagic_keymap
23def generate_layouts(cli):
24 """Generates the layouts.h file.
25 """
26 # Determine our keyboard(s)
27 if not cli.config.generate_layouts.keyboard:
28 cli.log.error('Missing paramater: --keyboard')
29 cli.subcommands['info'].print_help()
30 return False
31
32 if not is_keyboard(cli.config.generate_layouts.keyboard):
33 cli.log.error('Invalid keyboard: "%s"', cli.config.generate_layouts.keyboard)
34 return False
35
36 # Build the info.json file
37 kb_info_json = info_json(cli.config.generate_layouts.keyboard)
38
39 # Build the layouts.h file.
40 layouts_h_lines = ['/* This file was generated by `qmk generate-layouts`. Do not edit or copy.' ' */', '', '#pragma once']
41
42 if 'matrix_pins' in kb_info_json:
43 if 'direct' in kb_info_json['matrix_pins']:
44 col_num = len(kb_info_json['matrix_pins']['direct'][0])
45 row_num = len(kb_info_json['matrix_pins']['direct'])
46 elif 'cols' in kb_info_json['matrix_pins'] and 'rows' in kb_info_json['matrix_pins']:
47 col_num = len(kb_info_json['matrix_pins']['cols'])
48 row_num = len(kb_info_json['matrix_pins']['rows'])
49 else:
50 cli.log.error('%s: Invalid matrix config.', cli.config.generate_layouts.keyboard)
51 return False
52
53 for layout_name in kb_info_json['layouts']:
54 if kb_info_json['layouts'][layout_name]['c_macro']:
55 continue
56
57 if 'matrix' not in kb_info_json['layouts'][layout_name]['layout'][0]:
58 cli.log.debug('%s/%s: No matrix data!', cli.config.generate_layouts.keyboard, layout_name)
59 continue
60
61 layout_keys = []
62 layout_matrix = [['KC_NO' for i in range(col_num)] for i in range(row_num)]
63
64 for i, key in enumerate(kb_info_json['layouts'][layout_name]['layout']):
65 row = key['matrix'][0]
66 col = key['matrix'][1]
67 identifier = 'k%s%s' % (ROW_LETTERS[row], COL_LETTERS[col])
68
69 try:
70 layout_matrix[row][col] = identifier
71 layout_keys.append(identifier)
72 except IndexError:
73 key_name = key.get('label', identifier)
74 cli.log.error('Matrix data out of bounds for layout %s at index %s (%s): %s, %s', layout_name, i, key_name, row, col)
75 return False
76
77 layouts_h_lines.append('')
78 layouts_h_lines.append('#define %s(%s) {\\' % (layout_name, ', '.join(layout_keys)))
79
80 rows = ', \\\n'.join(['\t {' + ', '.join(row) + '}' for row in layout_matrix])
81 rows += ' \\'
82 layouts_h_lines.append(rows)
83 layouts_h_lines.append('}')
84
85 for alias, target in kb_info_json.get('layout_aliases', {}).items():
86 layouts_h_lines.append('')
87 layouts_h_lines.append('#define %s %s' % (alias, target))
88
89 # Show the results
90 layouts_h = '\n'.join(layouts_h_lines) + '\n'
91
92 if cli.args.output:
93 cli.args.output.parent.mkdir(parents=True, exist_ok=True)
94 if cli.args.output.exists():
95 cli.args.output.replace(cli.args.output.name + '.bak')
96 cli.args.output.write_text(layouts_h)
97
98 if not cli.args.quiet:
99 cli.log.info('Wrote info_config.h to %s.', cli.args.output)
100
101 else:
102 print(layouts_h)
diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py
new file mode 100755
index 000000000..af740f341
--- /dev/null
+++ b/lib/python/qmk/cli/generate/rules_mk.py
@@ -0,0 +1,91 @@
1"""Used by the make system to generate a rules.mk
2"""
3from pathlib import Path
4
5from dotty_dict import dotty
6from milc import cli
7
8from qmk.decorators import automagic_keyboard, automagic_keymap
9from qmk.info import _json_load, info_json
10from qmk.path import is_keyboard, normpath
11
12
13def process_mapping_rule(kb_info_json, rules_key, info_dict):
14 """Return the rules.mk line(s) for a mapping rule.
15 """
16 if not info_dict.get('to_c', True):
17 return None
18
19 info_key = info_dict['info_key']
20 key_type = info_dict.get('value_type', 'str')
21
22 try:
23 rules_value = kb_info_json[info_key]
24 except KeyError:
25 return None
26
27 if key_type == 'array':
28 return f'{rules_key} ?= {" ".join(rules_value)}'
29 elif key_type == 'bool':
30 return f'{rules_key} ?= {"on" if rules_value else "off"}'
31 elif key_type == 'mapping':
32 return '\n'.join([f'{key} ?= {value}' for key, value in rules_value.items()])
33
34 return f'{rules_key} ?= {rules_value}'
35
36
37@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
38@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
39@cli.argument('-kb', '--keyboard', help='Keyboard to generate config.h for.')
40@cli.subcommand('Used by the make system to generate info_config.h from info.json', hidden=True)
41@automagic_keyboard
42@automagic_keymap
43def generate_rules_mk(cli):
44 """Generates a rules.mk file from info.json.
45 """
46 if not cli.config.generate_rules_mk.keyboard:
47 cli.log.error('Missing paramater: --keyboard')
48 cli.subcommands['info'].print_help()
49 return False
50
51 if not is_keyboard(cli.config.generate_rules_mk.keyboard):
52 cli.log.error('Invalid keyboard: "%s"', cli.config.generate_rules_mk.keyboard)
53 return False
54
55 kb_info_json = dotty(info_json(cli.config.generate_rules_mk.keyboard))
56 info_rules_map = _json_load(Path('data/mappings/info_rules.json'))
57 rules_mk_lines = ['# This file was generated by `qmk generate-rules-mk`. Do not edit or copy.', '']
58
59 # Iterate through the info_rules map to generate basic rules
60 for rules_key, info_dict in info_rules_map.items():
61 new_entry = process_mapping_rule(kb_info_json, rules_key, info_dict)
62
63 if new_entry:
64 rules_mk_lines.append(new_entry)
65
66 # Iterate through features to enable/disable them
67 if 'features' in kb_info_json:
68 for feature, enabled in kb_info_json['features'].items():
69 if feature == 'bootmagic_lite' and enabled:
70 rules_mk_lines.append('BOOTMAGIC_ENABLE ?= lite')
71 else:
72 feature = feature.upper()
73 enabled = 'yes' if enabled else 'no'
74 rules_mk_lines.append(f'{feature}_ENABLE ?= {enabled}')
75
76 # Show the results
77 rules_mk = '\n'.join(rules_mk_lines) + '\n'
78
79 if cli.args.output:
80 cli.args.output.parent.mkdir(parents=True, exist_ok=True)
81 if cli.args.output.exists():
82 cli.args.output.replace(cli.args.output.name + '.bak')
83 cli.args.output.write_text(rules_mk)
84
85 if cli.args.quiet:
86 print(cli.args.output)
87 else:
88 cli.log.info('Wrote rules.mk to %s.', cli.args.output)
89
90 else:
91 print(rules_mk)
diff --git a/lib/python/qmk/cli/info.py b/lib/python/qmk/cli/info.py
index 9ab299a21..a7ce8abf0 100755
--- a/lib/python/qmk/cli/info.py
+++ b/lib/python/qmk/cli/info.py
@@ -7,6 +7,8 @@ import platform
7 7
8from milc import cli 8from milc import cli
9 9
10from qmk.info_json_encoder import InfoJSONEncoder
11from qmk.constants import COL_LETTERS, ROW_LETTERS
10from qmk.decorators import automagic_keyboard, automagic_keymap 12from qmk.decorators import automagic_keyboard, automagic_keymap
11from qmk.keyboard import render_layouts, render_layout 13from qmk.keyboard import render_layouts, render_layout
12from qmk.keymap import locate_keymap 14from qmk.keymap import locate_keymap
@@ -15,9 +17,6 @@ from qmk.path import is_keyboard
15 17
16platform_id = platform.platform().lower() 18platform_id = platform.platform().lower()
17 19
18ROW_LETTERS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop'
19COL_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijilmnopqrstuvwxyz'
20
21 20
22def show_keymap(kb_info_json, title_caps=True): 21def show_keymap(kb_info_json, title_caps=True):
23 """Render the keymap in ascii art. 22 """Render the keymap in ascii art.
@@ -30,7 +29,7 @@ def show_keymap(kb_info_json, title_caps=True):
30 else: 29 else:
31 cli.echo('{fg_blue}keymap_%s{fg_reset}:', cli.config.info.keymap) 30 cli.echo('{fg_blue}keymap_%s{fg_reset}:', cli.config.info.keymap)
32 31
33 keymap_data = json.load(keymap_path.open()) 32 keymap_data = json.load(keymap_path.open(encoding='utf-8'))
34 layout_name = keymap_data['layout'] 33 layout_name = keymap_data['layout']
35 34
36 for layer_num, layer in enumerate(keymap_data['layers']): 35 for layer_num, layer in enumerate(keymap_data['layers']):
@@ -58,7 +57,7 @@ def show_matrix(kb_info_json, title_caps=True):
58 # Build our label list 57 # Build our label list
59 labels = [] 58 labels = []
60 for key in layout['layout']: 59 for key in layout['layout']:
61 if key['matrix']: 60 if 'matrix' in key:
62 row = ROW_LETTERS[key['matrix'][0]] 61 row = ROW_LETTERS[key['matrix'][0]]
63 col = COL_LETTERS[key['matrix'][1]] 62 col = COL_LETTERS[key['matrix'][1]]
64 63
@@ -92,6 +91,9 @@ def print_friendly_output(kb_info_json):
92 cli.echo('{fg_blue}Size{fg_reset}: %s x %s' % (kb_info_json['width'], kb_info_json['height'])) 91 cli.echo('{fg_blue}Size{fg_reset}: %s x %s' % (kb_info_json['width'], kb_info_json['height']))
93 cli.echo('{fg_blue}Processor{fg_reset}: %s', kb_info_json.get('processor', 'Unknown')) 92 cli.echo('{fg_blue}Processor{fg_reset}: %s', kb_info_json.get('processor', 'Unknown'))
94 cli.echo('{fg_blue}Bootloader{fg_reset}: %s', kb_info_json.get('bootloader', 'Unknown')) 93 cli.echo('{fg_blue}Bootloader{fg_reset}: %s', kb_info_json.get('bootloader', 'Unknown'))
94 if 'layout_aliases' in kb_info_json:
95 aliases = [f'{key}={value}' for key, value in kb_info_json['layout_aliases'].items()]
96 cli.echo('{fg_blue}Layout aliases:{fg_reset} %s' % (', '.join(aliases),))
95 97
96 if cli.config.info.layouts: 98 if cli.config.info.layouts:
97 show_layouts(kb_info_json, True) 99 show_layouts(kb_info_json, True)
@@ -149,7 +151,7 @@ def info(cli):
149 151
150 # Output in the requested format 152 # Output in the requested format
151 if cli.args.format == 'json': 153 if cli.args.format == 'json':
152 print(json.dumps(kb_info_json)) 154 print(json.dumps(kb_info_json, cls=InfoJSONEncoder))
153 elif cli.args.format == 'text': 155 elif cli.args.format == 'text':
154 print_text_output(kb_info_json) 156 print_text_output(kb_info_json)
155 elif cli.args.format == 'friendly': 157 elif cli.args.format == 'friendly':
diff --git a/lib/python/qmk/cli/json/__init__.py b/lib/python/qmk/cli/json/__init__.py
deleted file mode 100644
index f4ebfc45b..000000000
--- a/lib/python/qmk/cli/json/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
1"""QMK CLI JSON Subcommands
2
3We list each subcommand here explicitly because all the reliable ways of searching for modules are slow and delay startup.
4"""
5from . import keymap
diff --git a/lib/python/qmk/cli/json/keymap.py b/lib/python/qmk/cli/json/keymap.py
deleted file mode 100755
index 2af9faaa7..000000000
--- a/lib/python/qmk/cli/json/keymap.py
+++ /dev/null
@@ -1,16 +0,0 @@
1"""Generate a keymap.c from a configurator export.
2"""
3from pathlib import Path
4
5from milc import cli
6
7
8@cli.argument('-o', '--output', arg_only=True, type=Path, help='File to write to')
9@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
10@cli.argument('filename', arg_only=True, help='Configurator JSON file')
11@cli.subcommand('Creates a keymap.c from a QMK Configurator export.', hidden=True)
12def json_keymap(cli):
13 """Renamed to `qmk json2c`.
14 """
15 cli.log.error('This command has been renamed to `qmk json2c`.')
16 return False
diff --git a/lib/python/qmk/cli/kle2json.py b/lib/python/qmk/cli/kle2json.py
index 3d1bb8c43..3bb744358 100755
--- a/lib/python/qmk/cli/kle2json.py
+++ b/lib/python/qmk/cli/kle2json.py
@@ -3,25 +3,12 @@
3import json 3import json
4import os 4import os
5from pathlib import Path 5from pathlib import Path
6from decimal import Decimal
7from collections import OrderedDict
8 6
9from milc import cli 7from milc import cli
10from kle2xy import KLE2xy 8from kle2xy import KLE2xy
11 9
12from qmk.converter import kle2qmk 10from qmk.converter import kle2qmk
13 11from qmk.info_json_encoder import InfoJSONEncoder
14
15class CustomJSONEncoder(json.JSONEncoder):
16 def default(self, obj):
17 try:
18 if isinstance(obj, Decimal):
19 if obj % 2 in (Decimal(0), Decimal(1)):
20 return int(obj)
21 return float(obj)
22 except TypeError:
23 pass
24 return json.JSONEncoder.default(self, obj)
25 12
26 13
27@cli.argument('filename', help='The KLE raw txt to convert') 14@cli.argument('filename', help='The KLE raw txt to convert')
@@ -40,7 +27,7 @@ def kle2json(cli):
40 cli.log.error('File {fg_cyan}%s{style_reset_all} was not found.', file_path) 27 cli.log.error('File {fg_cyan}%s{style_reset_all} was not found.', file_path)
41 return False 28 return False
42 out_path = file_path.parent 29 out_path = file_path.parent
43 raw_code = file_path.open().read() 30 raw_code = file_path.read_text(encoding='utf-8')
44 # Check if info.json exists, allow overwrite with force 31 # Check if info.json exists, allow overwrite with force
45 if Path(out_path, "info.json").exists() and not cli.args.force: 32 if Path(out_path, "info.json").exists() and not cli.args.force:
46 cli.log.error('File {fg_cyan}%s/info.json{style_reset_all} already exists, use -f or --force to overwrite.', out_path) 33 cli.log.error('File {fg_cyan}%s/info.json{style_reset_all} already exists, use -f or --force to overwrite.', out_path)
@@ -52,24 +39,22 @@ def kle2json(cli):
52 cli.log.error('Could not parse KLE raw data: %s', raw_code) 39 cli.log.error('Could not parse KLE raw data: %s', raw_code)
53 cli.log.exception(e) 40 cli.log.exception(e)
54 return False 41 return False
55 keyboard = OrderedDict( 42 keyboard = {
56 keyboard_name=kle.name, 43 'keyboard_name': kle.name,
57 url='', 44 'url': '',
58 maintainer='qmk', 45 'maintainer': 'qmk',
59 width=kle.columns, 46 'width': kle.columns,
60 height=kle.rows, 47 'height': kle.rows,
61 layouts={'LAYOUT': { 48 'layouts': {
62 'layout': 'LAYOUT_JSON_HERE' 49 'LAYOUT': {
63 }}, 50 'layout': kle2qmk(kle)
64 ) 51 }
65 # Initialize keyboard with json encoded from ordered dict 52 },
66 keyboard = json.dumps(keyboard, indent=4, separators=(', ', ': '), sort_keys=False, cls=CustomJSONEncoder) 53 }
67 # Initialize layout with kle2qmk from converter module 54
68 layout = json.dumps(kle2qmk(kle), separators=(', ', ':'), cls=CustomJSONEncoder)
69 # Replace layout in keyboard json
70 keyboard = keyboard.replace('"LAYOUT_JSON_HERE"', layout)
71 # Write our info.json 55 # Write our info.json
72 file = open(out_path / "info.json", "w") 56 keyboard = json.dumps(keyboard, indent=4, separators=(', ', ': '), sort_keys=False, cls=InfoJSONEncoder)
73 file.write(keyboard) 57 info_json_file = out_path / 'info.json'
74 file.close() 58
59 info_json_file.write_text(keyboard)
75 cli.log.info('Wrote out {fg_cyan}%s/info.json', out_path) 60 cli.log.info('Wrote out {fg_cyan}%s/info.json', out_path)
diff --git a/lib/python/qmk/constants.py b/lib/python/qmk/constants.py
index 2ddaa568a..3ed69f3bf 100644
--- a/lib/python/qmk/constants.py
+++ b/lib/python/qmk/constants.py
@@ -10,8 +10,8 @@ QMK_FIRMWARE = Path.cwd()
10MAX_KEYBOARD_SUBFOLDERS = 5 10MAX_KEYBOARD_SUBFOLDERS = 5
11 11
12# Supported processor types 12# Supported processor types
13CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F411' 13CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F411', 'STM32G431', 'STM32G474'
14LUFA_PROCESSORS = 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None 14LUFA_PROCESSORS = 'at90usb162', 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None
15VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85' 15VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85'
16 16
17# Common format strings 17# Common format strings
@@ -19,6 +19,17 @@ DATE_FORMAT = '%Y-%m-%d'
19DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S %Z' 19DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S %Z'
20TIME_FORMAT = '%H:%M:%S' 20TIME_FORMAT = '%H:%M:%S'
21 21
22# Used when generating matrix locations
23COL_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijilmnopqrstuvwxyz'
24ROW_LETTERS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop'
25
26# Mapping between info.json and config.h keys
27LED_INDICATORS = {
28 'caps_lock': 'LED_CAPS_LOCK_PIN',
29 'num_lock': 'LED_NUM_LOCK_PIN',
30 'scroll_lock': 'LED_SCROLL_LOCK_PIN',
31}
32
22# Constants that should match their counterparts in make 33# Constants that should match their counterparts in make
23BUILD_DIR = environ.get('BUILD_DIR', '.build') 34BUILD_DIR = environ.get('BUILD_DIR', '.build')
24KEYBOARD_OUTPUT_PREFIX = f'{BUILD_DIR}/obj_' 35KEYBOARD_OUTPUT_PREFIX = f'{BUILD_DIR}/obj_'
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index f476dc666..cf5dc6640 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -1,9 +1,13 @@
1"""Functions that help us generate and use info.json files. 1"""Functions that help us generate and use info.json files.
2""" 2"""
3import json 3import json
4from collections.abc import Mapping
4from glob import glob 5from glob import glob
5from pathlib import Path 6from pathlib import Path
6 7
8import hjson
9import jsonschema
10from dotty_dict import dotty
7from milc import cli 11from milc import cli
8 12
9from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS 13from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS
@@ -13,6 +17,9 @@ from qmk.keymap import list_keymaps
13from qmk.makefile import parse_rules_mk_file 17from qmk.makefile import parse_rules_mk_file
14from qmk.math import compute 18from qmk.math import compute
15 19
20true_values = ['1', 'on', 'yes']
21false_values = ['0', 'off', 'no']
22
16 23
17def info_json(keyboard): 24def info_json(keyboard):
18 """Generate the info.json data for a specific keyboard. 25 """Generate the info.json data for a specific keyboard.
@@ -38,8 +45,14 @@ def info_json(keyboard):
38 info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'} 45 info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'}
39 46
40 # Populate layout data 47 # Populate layout data
41 for layout_name, layout_json in _find_all_layouts(info_data, keyboard, rules).items(): 48 layouts, aliases = _find_all_layouts(info_data, keyboard)
49
50 if aliases:
51 info_data['layout_aliases'] = aliases
52
53 for layout_name, layout_json in layouts.items():
42 if not layout_name.startswith('LAYOUT_kc'): 54 if not layout_name.startswith('LAYOUT_kc'):
55 layout_json['c_macro'] = True
43 info_data['layouts'][layout_name] = layout_json 56 info_data['layouts'][layout_name] = layout_json
44 57
45 # Merge in the data from info.json, config.h, and rules.mk 58 # Merge in the data from info.json, config.h, and rules.mk
@@ -47,54 +60,259 @@ def info_json(keyboard):
47 info_data = _extract_config_h(info_data) 60 info_data = _extract_config_h(info_data)
48 info_data = _extract_rules_mk(info_data) 61 info_data = _extract_rules_mk(info_data)
49 62
63 # Validate against the jsonschema
64 try:
65 keyboard_api_validate(info_data)
66
67 except jsonschema.ValidationError as e:
68 json_path = '.'.join([str(p) for p in e.absolute_path])
69 cli.log.error('Invalid API data: %s: %s: %s', keyboard, json_path, e.message)
70 exit()
71
72 # Make sure we have at least one layout
73 if not info_data.get('layouts'):
74 _log_error(info_data, 'No LAYOUTs defined! Need at least one layout defined in the keyboard.h or info.json.')
75
76 # Make sure we supply layout macros for the community layouts we claim to support
77 # FIXME(skullydazed): This should be populated into info.json and read from there instead
78 if 'LAYOUTS' in rules and info_data.get('layouts'):
79 # Match these up against the supplied layouts
80 supported_layouts = rules['LAYOUTS'].strip().split()
81 for layout_name in sorted(info_data['layouts']):
82 layout_name = layout_name[7:]
83
84 if layout_name in supported_layouts:
85 supported_layouts.remove(layout_name)
86
87 if supported_layouts:
88 for supported_layout in supported_layouts:
89 _log_error(info_data, 'Claims to support community layout %s but no LAYOUT_%s() macro found' % (supported_layout, supported_layout))
90
50 return info_data 91 return info_data
51 92
52 93
53def _extract_config_h(info_data): 94def _json_load(json_file):
54 """Pull some keyboard information from existing rules.mk files 95 """Load a json file from disk.
96
97 Note: file must be a Path object.
98 """
99 try:
100 return hjson.load(json_file.open(encoding='utf-8'))
101
102 except json.decoder.JSONDecodeError as e:
103 cli.log.error('Invalid JSON encountered attempting to load {fg_cyan}%s{fg_reset}:\n\t{fg_red}%s', json_file, e)
104 exit(1)
105
106
107def _jsonschema(schema_name):
108 """Read a jsonschema file from disk.
109
110 FIXME(skullydazed/anyone): Refactor to make this a public function.
111 """
112 schema_path = Path(f'data/schemas/{schema_name}.jsonschema')
113
114 if not schema_path.exists():
115 schema_path = Path('data/schemas/false.jsonschema')
116
117 return _json_load(schema_path)
118
119
120def keyboard_validate(data):
121 """Validates data against the keyboard jsonschema.
122 """
123 schema = _jsonschema('keyboard')
124 validator = jsonschema.Draft7Validator(schema).validate
125
126 return validator(data)
127
128
129def keyboard_api_validate(data):
130 """Validates data against the api_keyboard jsonschema.
131 """
132 base = _jsonschema('keyboard')
133 relative = _jsonschema('api_keyboard')
134 resolver = jsonschema.RefResolver.from_schema(base)
135 validator = jsonschema.Draft7Validator(relative, resolver=resolver).validate
136
137 return validator(data)
138
139
140def _extract_features(info_data, rules):
141 """Find all the features enabled in rules.mk.
142 """
143 # Special handling for bootmagic which also supports a "lite" mode.
144 if rules.get('BOOTMAGIC_ENABLE') == 'lite':
145 rules['BOOTMAGIC_LITE_ENABLE'] = 'on'
146 del rules['BOOTMAGIC_ENABLE']
147 if rules.get('BOOTMAGIC_ENABLE') == 'full':
148 rules['BOOTMAGIC_ENABLE'] = 'on'
149
150 # Skip non-boolean features we haven't implemented special handling for
151 for feature in 'HAPTIC_ENABLE', 'QWIIC_ENABLE':
152 if rules.get(feature):
153 del rules[feature]
154
155 # Process the rest of the rules as booleans
156 for key, value in rules.items():
157 if key.endswith('_ENABLE'):
158 key = '_'.join(key.split('_')[:-1]).lower()
159 value = True if value.lower() in true_values else False if value.lower() in false_values else value
160
161 if 'config_h_features' not in info_data:
162 info_data['config_h_features'] = {}
163
164 if 'features' not in info_data:
165 info_data['features'] = {}
166
167 if key in info_data['features']:
168 _log_warning(info_data, 'Feature %s is specified in both info.json and rules.mk, the rules.mk value wins.' % (key,))
169
170 info_data['features'][key] = value
171 info_data['config_h_features'][key] = value
172
173 return info_data
174
175
176def _pin_name(pin):
177 """Returns the proper representation for a pin.
178 """
179 pin = pin.strip()
180
181 if not pin:
182 return None
183
184 elif pin.isdigit():
185 return int(pin)
186
187 elif pin == 'NO_PIN':
188 return None
189
190 elif pin[0] in 'ABCDEFGHIJK' and pin[1].isdigit():
191 return pin
192
193 raise ValueError(f'Invalid pin: {pin}')
194
195
196def _extract_pins(pins):
197 """Returns a list of pins from a comma separated string of pins.
198 """
199 return [_pin_name(pin) for pin in pins.split(',')]
200
201
202def _extract_direct_matrix(info_data, direct_pins):
203 """
204 """
205 info_data['matrix_pins'] = {}
206 direct_pin_array = []
207
208 while direct_pins[-1] != '}':
209 direct_pins = direct_pins[:-1]
210
211 for row in direct_pins.split('},{'):
212 if row.startswith('{'):
213 row = row[1:]
214
215 if row.endswith('}'):
216 row = row[:-1]
217
218 direct_pin_array.append([])
219
220 for pin in row.split(','):
221 if pin == 'NO_PIN':
222 pin = None
223
224 direct_pin_array[-1].append(pin)
225
226 return direct_pin_array
227
228
229def _extract_matrix_info(info_data, config_c):
230 """Populate the matrix information.
55 """ 231 """
56 config_c = config_h(info_data['keyboard_folder'])
57 row_pins = config_c.get('MATRIX_ROW_PINS', '').replace('{', '').replace('}', '').strip() 232 row_pins = config_c.get('MATRIX_ROW_PINS', '').replace('{', '').replace('}', '').strip()
58 col_pins = config_c.get('MATRIX_COL_PINS', '').replace('{', '').replace('}', '').strip() 233 col_pins = config_c.get('MATRIX_COL_PINS', '').replace('{', '').replace('}', '').strip()
59 direct_pins = config_c.get('DIRECT_PINS', '').replace(' ', '')[1:-1] 234 direct_pins = config_c.get('DIRECT_PINS', '').replace(' ', '')[1:-1]
60 235
61 info_data['diode_direction'] = config_c.get('DIODE_DIRECTION') 236 if 'MATRIX_ROWS' in config_c and 'MATRIX_COLS' in config_c:
62 info_data['matrix_size'] = { 237 if 'matrix_size' in info_data:
63 'rows': compute(config_c.get('MATRIX_ROWS', '0')), 238 _log_warning(info_data, 'Matrix size is specified in both info.json and config.h, the config.h values win.')
64 'cols': compute(config_c.get('MATRIX_COLS', '0')), 239
65 } 240 info_data['matrix_size'] = {
66 info_data['matrix_pins'] = {} 241 'cols': compute(config_c.get('MATRIX_COLS', '0')),
242 'rows': compute(config_c.get('MATRIX_ROWS', '0')),
243 }
67 244
68 if row_pins: 245 if row_pins and col_pins:
69 info_data['matrix_pins']['rows'] = row_pins.split(',') 246 if 'matrix_pins' in info_data:
70 if col_pins: 247 _log_warning(info_data, 'Matrix pins are specified in both info.json and config.h, the config.h values win.')
71 info_data['matrix_pins']['cols'] = col_pins.split(',') 248
249 info_data['matrix_pins'] = {
250 'cols': _extract_pins(col_pins),
251 'rows': _extract_pins(row_pins),
252 }
72 253
73 if direct_pins: 254 if direct_pins:
74 direct_pin_array = [] 255 if 'matrix_pins' in info_data:
75 for row in direct_pins.split('},{'): 256 _log_warning(info_data, 'Direct pins are specified in both info.json and config.h, the config.h values win.')
76 if row.startswith('{'): 257
77 row = row[1:] 258 info_data['matrix_pins']['direct'] = _extract_direct_matrix(info_data, direct_pins)
78 if row.endswith('}'): 259
79 row = row[:-1] 260 return info_data
261
80 262
81 direct_pin_array.append([]) 263def _extract_config_h(info_data):
264 """Pull some keyboard information from existing config.h files
265 """
266 config_c = config_h(info_data['keyboard_folder'])
82 267
83 for pin in row.split(','): 268 # Pull in data from the json map
84 if pin == 'NO_PIN': 269 dotty_info = dotty(info_data)
85 pin = None 270 info_config_map = _json_load(Path('data/mappings/info_config.json'))
86 271
87 direct_pin_array[-1].append(pin) 272 for config_key, info_dict in info_config_map.items():
273 info_key = info_dict['info_key']
274 key_type = info_dict.get('value_type', 'str')
88 275
89 info_data['matrix_pins']['direct'] = direct_pin_array 276 try:
277 if config_key in config_c and info_dict.get('to_json', True):
278 if dotty_info.get(info_key) and info_dict.get('warn_duplicate', True):
279 _log_warning(info_data, '%s in config.h is overwriting %s in info.json' % (config_key, info_key))
90 280
91 info_data['usb'] = { 281 if key_type.startswith('array'):
92 'vid': config_c.get('VENDOR_ID'), 282 if '.' in key_type:
93 'pid': config_c.get('PRODUCT_ID'), 283 key_type, array_type = key_type.split('.', 1)
94 'device_ver': config_c.get('DEVICE_VER'), 284 else:
95 'manufacturer': config_c.get('MANUFACTURER'), 285 array_type = None
96 'product': config_c.get('PRODUCT'), 286
97 } 287 config_value = config_c[config_key].replace('{', '').replace('}', '').strip()
288
289 if array_type == 'int':
290 dotty_info[info_key] = list(map(int, config_value.split(',')))
291 else:
292 dotty_info[info_key] = config_value.split(',')
293
294 elif key_type == 'bool':
295 dotty_info[info_key] = config_c[config_key] in true_values
296
297 elif key_type == 'hex':
298 dotty_info[info_key] = '0x' + config_c[config_key][2:].upper()
299
300 elif key_type == 'list':
301 dotty_info[info_key] = config_c[config_key].split()
302
303 elif key_type == 'int':
304 dotty_info[info_key] = int(config_c[config_key])
305
306 else:
307 dotty_info[info_key] = config_c[config_key]
308
309 except Exception as e:
310 _log_warning(info_data, f'{config_key}->{info_key}: {e}')
311
312 info_data.update(dotty_info)
313
314 # Pull data that easily can't be mapped in json
315 _extract_matrix_info(info_data, config_c)
98 316
99 return info_data 317 return info_data
100 318
@@ -103,63 +321,143 @@ def _extract_rules_mk(info_data):
103 """Pull some keyboard information from existing rules.mk files 321 """Pull some keyboard information from existing rules.mk files
104 """ 322 """
105 rules = rules_mk(info_data['keyboard_folder']) 323 rules = rules_mk(info_data['keyboard_folder'])
106 mcu = rules.get('MCU') 324 info_data['processor'] = rules.get('MCU', info_data.get('processor', 'atmega32u4'))
325
326 if info_data['processor'] in CHIBIOS_PROCESSORS:
327 arm_processor_rules(info_data, rules)
328
329 elif info_data['processor'] in LUFA_PROCESSORS + VUSB_PROCESSORS:
330 avr_processor_rules(info_data, rules)
331
332 else:
333 cli.log.warning("%s: Unknown MCU: %s" % (info_data['keyboard_folder'], info_data['processor']))
334 unknown_processor_rules(info_data, rules)
335
336 # Pull in data from the json map
337 dotty_info = dotty(info_data)
338 info_rules_map = _json_load(Path('data/mappings/info_rules.json'))
339
340 for rules_key, info_dict in info_rules_map.items():
341 info_key = info_dict['info_key']
342 key_type = info_dict.get('value_type', 'str')
343
344 try:
345 if rules_key in rules and info_dict.get('to_json', True):
346 if dotty_info.get(info_key) and info_dict.get('warn_duplicate', True):
347 _log_warning(info_data, '%s in rules.mk is overwriting %s in info.json' % (rules_key, info_key))
348
349 if key_type.startswith('array'):
350 if '.' in key_type:
351 key_type, array_type = key_type.split('.', 1)
352 else:
353 array_type = None
354
355 rules_value = rules[rules_key].replace('{', '').replace('}', '').strip()
356
357 if array_type == 'int':
358 dotty_info[info_key] = list(map(int, rules_value.split(',')))
359 else:
360 dotty_info[info_key] = rules_value.split(',')
361
362 elif key_type == 'list':
363 dotty_info[info_key] = rules[rules_key].split()
364
365 elif key_type == 'bool':
366 dotty_info[info_key] = rules[rules_key] in true_values
107 367
108 if mcu in CHIBIOS_PROCESSORS: 368 elif key_type == 'hex':
109 return arm_processor_rules(info_data, rules) 369 dotty_info[info_key] = '0x' + rules[rules_key][2:].upper()
110 370
111 elif mcu in LUFA_PROCESSORS + VUSB_PROCESSORS: 371 elif key_type == 'int':
112 return avr_processor_rules(info_data, rules) 372 dotty_info[info_key] = int(rules[rules_key])
113 373
114 msg = "Unknown MCU: " + str(mcu) 374 else:
375 dotty_info[info_key] = rules[rules_key]
115 376
116 _log_warning(info_data, msg) 377 except Exception as e:
378 _log_warning(info_data, f'{rules_key}->{info_key}: {e}')
379
380 info_data.update(dotty_info)
381
382 # Merge in config values that can't be easily mapped
383 _extract_features(info_data, rules)
384
385 return info_data
117 386
118 return unknown_processor_rules(info_data, rules) 387
388def _merge_layouts(info_data, new_info_data):
389 """Merge new_info_data into info_data in an intelligent way.
390 """
391 for layout_name, layout_json in new_info_data['layouts'].items():
392 if layout_name in info_data['layouts']:
393 # Pull in layouts we have a macro for
394 if len(info_data['layouts'][layout_name]['layout']) != len(layout_json['layout']):
395 msg = '%s: %s: Number of elements in info.json does not match! info.json:%s != %s:%s'
396 _log_error(info_data, msg % (info_data['keyboard_folder'], layout_name, len(layout_json['layout']), layout_name, len(info_data['layouts'][layout_name]['layout'])))
397 else:
398 for i, key in enumerate(info_data['layouts'][layout_name]['layout']):
399 key.update(layout_json['layout'][i])
400 else:
401 # Pull in layouts that have matrix data
402 missing_matrix = False
403 for key in layout_json.get('layout', {}):
404 if 'matrix' not in key:
405 missing_matrix = True
406
407 if not missing_matrix:
408 if layout_name in info_data['layouts']:
409 # Update an existing layout with new data
410 for i, key in enumerate(info_data['layouts'][layout_name]['layout']):
411 key.update(layout_json['layout'][i])
412
413 else:
414 # Copy in the new layout wholesale
415 layout_json['c_macro'] = False
416 info_data['layouts'][layout_name] = layout_json
417
418 return info_data
119 419
120 420
121def _search_keyboard_h(path): 421def _search_keyboard_h(path):
122 current_path = Path('keyboards/') 422 current_path = Path('keyboards/')
423 aliases = {}
123 layouts = {} 424 layouts = {}
425
124 for directory in path.parts: 426 for directory in path.parts:
125 current_path = current_path / directory 427 current_path = current_path / directory
126 keyboard_h = '%s.h' % (directory,) 428 keyboard_h = '%s.h' % (directory,)
127 keyboard_h_path = current_path / keyboard_h 429 keyboard_h_path = current_path / keyboard_h
128 if keyboard_h_path.exists(): 430 if keyboard_h_path.exists():
129 layouts.update(find_layouts(keyboard_h_path)) 431 new_layouts, new_aliases = find_layouts(keyboard_h_path)
432 layouts.update(new_layouts)
433
434 for alias, alias_text in new_aliases.items():
435 if alias_text in layouts:
436 aliases[alias] = alias_text
130 437
131 return layouts 438 return layouts, aliases
132 439
133 440
134def _find_all_layouts(info_data, keyboard, rules): 441def _find_all_layouts(info_data, keyboard):
135 """Looks for layout macros associated with this keyboard. 442 """Looks for layout macros associated with this keyboard.
136 """ 443 """
137 layouts = _search_keyboard_h(Path(keyboard)) 444 layouts, aliases = _search_keyboard_h(Path(keyboard))
138 445
139 if not layouts: 446 if not layouts:
140 # If we didn't find any layouts above we widen our search. This is error 447 # If we don't find any layouts from info.json or keyboard.h we widen our search. This is error prone which is why we want to encourage people to follow the standard above.
141 # prone which is why we want to encourage people to follow the standard above. 448 info_data['parse_warnings'].append('%s: Falling back to searching for KEYMAP/LAYOUT macros.' % (keyboard))
142 _log_warning(info_data, 'Falling back to searching for KEYMAP/LAYOUT macros.') 449
143 for file in glob('keyboards/%s/*.h' % keyboard): 450 for file in glob('keyboards/%s/*.h' % keyboard):
144 if file.endswith('.h'): 451 if file.endswith('.h'):
145 these_layouts = find_layouts(file) 452 these_layouts, these_aliases = find_layouts(file)
453
146 if these_layouts: 454 if these_layouts:
147 layouts.update(these_layouts) 455 layouts.update(these_layouts)
148 456
149 if 'LAYOUTS' in rules: 457 if these_aliases:
150 # Match these up against the supplied layouts 458 aliases.update(these_aliases)
151 supported_layouts = rules['LAYOUTS'].strip().split()
152 for layout_name in sorted(layouts):
153 if not layout_name.startswith('LAYOUT_'):
154 continue
155 layout_name = layout_name[7:]
156 if layout_name in supported_layouts:
157 supported_layouts.remove(layout_name)
158
159 if supported_layouts:
160 _log_error(info_data, 'Missing LAYOUT() macro for %s' % (', '.join(supported_layouts)))
161 459
162 return layouts 460 return layouts, aliases
163 461
164 462
165def _log_error(info_data, message): 463def _log_error(info_data, message):
@@ -180,13 +478,13 @@ def arm_processor_rules(info_data, rules):
180 """Setup the default info for an ARM board. 478 """Setup the default info for an ARM board.
181 """ 479 """
182 info_data['processor_type'] = 'arm' 480 info_data['processor_type'] = 'arm'
183 info_data['bootloader'] = rules['BOOTLOADER'] if 'BOOTLOADER' in rules else 'unknown'
184 info_data['processor'] = rules['MCU'] if 'MCU' in rules else 'unknown'
185 info_data['protocol'] = 'ChibiOS' 481 info_data['protocol'] = 'ChibiOS'
186 482
187 if info_data['bootloader'] == 'unknown': 483 if 'bootloader' not in info_data:
188 if 'STM32' in info_data['processor']: 484 if 'STM32' in info_data['processor']:
189 info_data['bootloader'] = 'stm32-dfu' 485 info_data['bootloader'] = 'stm32-dfu'
486 else:
487 info_data['bootloader'] = 'unknown'
190 488
191 if 'STM32' in info_data['processor']: 489 if 'STM32' in info_data['processor']:
192 info_data['platform'] = 'STM32' 490 info_data['platform'] = 'STM32'
@@ -202,11 +500,12 @@ def avr_processor_rules(info_data, rules):
202 """Setup the default info for an AVR board. 500 """Setup the default info for an AVR board.
203 """ 501 """
204 info_data['processor_type'] = 'avr' 502 info_data['processor_type'] = 'avr'
205 info_data['bootloader'] = rules['BOOTLOADER'] if 'BOOTLOADER' in rules else 'atmel-dfu'
206 info_data['platform'] = rules['ARCH'] if 'ARCH' in rules else 'unknown' 503 info_data['platform'] = rules['ARCH'] if 'ARCH' in rules else 'unknown'
207 info_data['processor'] = rules['MCU'] if 'MCU' in rules else 'unknown'
208 info_data['protocol'] = 'V-USB' if rules.get('MCU') in VUSB_PROCESSORS else 'LUFA' 504 info_data['protocol'] = 'V-USB' if rules.get('MCU') in VUSB_PROCESSORS else 'LUFA'
209 505
506 if 'bootloader' not in info_data:
507 info_data['bootloader'] = 'atmel-dfu'
508
210 # FIXME(fauxpark/anyone): Eventually we should detect the protocol by looking at PROTOCOL inherited from mcu_selection.mk: 509 # FIXME(fauxpark/anyone): Eventually we should detect the protocol by looking at PROTOCOL inherited from mcu_selection.mk:
211 # info_data['protocol'] = 'V-USB' if rules.get('PROTOCOL') == 'VUSB' else 'LUFA' 510 # info_data['protocol'] = 'V-USB' if rules.get('PROTOCOL') == 'VUSB' else 'LUFA'
212 511
@@ -225,38 +524,52 @@ def unknown_processor_rules(info_data, rules):
225 return info_data 524 return info_data
226 525
227 526
527def deep_update(origdict, newdict):
528 """Update a dictionary in place, recursing to do a deep copy.
529 """
530 for key, value in newdict.items():
531 if isinstance(value, Mapping):
532 origdict[key] = deep_update(origdict.get(key, {}), value)
533
534 else:
535 origdict[key] = value
536
537 return origdict
538
539
228def merge_info_jsons(keyboard, info_data): 540def merge_info_jsons(keyboard, info_data):
229 """Return a merged copy of all the info.json files for a keyboard. 541 """Return a merged copy of all the info.json files for a keyboard.
230 """ 542 """
231 for info_file in find_info_json(keyboard): 543 for info_file in find_info_json(keyboard):
232 # Load and validate the JSON data 544 # Load and validate the JSON data
233 try: 545 new_info_data = _json_load(info_file)
234 with info_file.open('r') as info_fd:
235 new_info_data = json.load(info_fd)
236 except Exception as e:
237 _log_error(info_data, "Invalid JSON in file %s: %s: %s" % (str(info_file), e.__class__.__name__, e))
238 continue
239 546
240 if not isinstance(new_info_data, dict): 547 if not isinstance(new_info_data, dict):
241 _log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),)) 548 _log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),))
242 continue 549 continue
243 550
244 # Copy whitelisted keys into `info_data` 551 try:
245 for key in ('keyboard_name', 'manufacturer', 'identifier', 'url', 'maintainer', 'processor', 'bootloader', 'width', 'height'): 552 keyboard_validate(new_info_data)
246 if key in new_info_data: 553 except jsonschema.ValidationError as e:
247 info_data[key] = new_info_data[key] 554 json_path = '.'.join([str(p) for p in e.absolute_path])
555 cli.log.error('Not including data from file: %s', info_file)
556 cli.log.error('\t%s: %s', json_path, e.message)
557 continue
248 558
249 # Merge the layouts in 559 # Merge layout data in
560 for layout_name, layout in new_info_data.get('layouts', {}).items():
561 if layout_name in info_data['layouts']:
562 for new_key, existing_key in zip(layout['layout'], info_data['layouts'][layout_name]['layout']):
563 existing_key.update(new_key)
564 else:
565 layout['c_macro'] = False
566 info_data['layouts'][layout_name] = layout
567
568 # Update info_data with the new data
250 if 'layouts' in new_info_data: 569 if 'layouts' in new_info_data:
251 for layout_name, json_layout in new_info_data['layouts'].items(): 570 del (new_info_data['layouts'])
252 # Only pull in layouts we have a macro for 571
253 if layout_name in info_data['layouts']: 572 deep_update(info_data, new_info_data)
254 if info_data['layouts'][layout_name]['key_count'] != len(json_layout['layout']):
255 msg = '%s: Number of elements in info.json does not match! info.json:%s != %s:%s'
256 _log_error(info_data, msg % (layout_name, len(json_layout['layout']), layout_name, len(info_data['layouts'][layout_name]['layout'])))
257 else:
258 for i, key in enumerate(info_data['layouts'][layout_name]['layout']):
259 key.update(json_layout['layout'][i])
260 573
261 return info_data 574 return info_data
262 575
diff --git a/lib/python/qmk/info_json_encoder.py b/lib/python/qmk/info_json_encoder.py
new file mode 100755
index 000000000..60dae7247
--- /dev/null
+++ b/lib/python/qmk/info_json_encoder.py
@@ -0,0 +1,96 @@
1"""Class that pretty-prints QMK info.json files.
2"""
3import json
4from decimal import Decimal
5
6
7class InfoJSONEncoder(json.JSONEncoder):
8 """Custom encoder to make info.json's a little nicer to work with.
9 """
10 container_types = (list, tuple, dict)
11 indentation_char = " "
12
13 def __init__(self, *args, **kwargs):
14 super().__init__(*args, **kwargs)
15 self.indentation_level = 0
16
17 if not self.indent:
18 self.indent = 4
19
20 def encode(self, obj):
21 """Encode JSON objects for QMK.
22 """
23 if isinstance(obj, Decimal):
24 if obj == int(obj): # I can't believe Decimal objects don't have .is_integer()
25 return int(obj)
26 return float(obj)
27
28 elif isinstance(obj, (list, tuple)):
29 if self._primitives_only(obj):
30 return "[" + ", ".join(self.encode(element) for element in obj) + "]"
31
32 else:
33 self.indentation_level += 1
34 output = [self.indent_str + self.encode(element) for element in obj]
35 self.indentation_level -= 1
36 return "[\n" + ",\n".join(output) + "\n" + self.indent_str + "]"
37
38 elif isinstance(obj, dict):
39 if obj:
40 if self.indentation_level == 4:
41 # These are part of a layout, put them on a single line.
42 return "{ " + ", ".join(f"{self.encode(key)}: {self.encode(element)}" for key, element in sorted(obj.items())) + " }"
43
44 else:
45 self.indentation_level += 1
46 output = [self.indent_str + f"{json.dumps(key)}: {self.encode(value)}" for key, value in sorted(obj.items(), key=self.sort_root_dict)]
47 self.indentation_level -= 1
48 return "{\n" + ",\n".join(output) + "\n" + self.indent_str + "}"
49 else:
50 return "{}"
51 else:
52 return super().encode(obj)
53
54 def _primitives_only(self, obj):
55 """Returns true if the object doesn't have any container type objects (list, tuple, dict).
56 """
57 if isinstance(obj, dict):
58 obj = obj.values()
59
60 return not any(isinstance(element, self.container_types) for element in obj)
61
62 def sort_root_dict(self, key):
63 """Forces layout to the back of the sort order.
64 """
65 key = key[0]
66
67 if self.indentation_level == 1:
68 if key == 'manufacturer':
69 return '10keyboard_name'
70
71 elif key == 'keyboard_name':
72 return '11keyboard_name'
73
74 elif key == 'maintainer':
75 return '12maintainer'
76
77 elif key in ('height', 'width'):
78 return '40' + str(key)
79
80 elif key == 'community_layouts':
81 return '97community_layouts'
82
83 elif key == 'layout_aliases':
84 return '98layout_aliases'
85
86 elif key == 'layouts':
87 return '99layouts'
88
89 else:
90 return '50' + str(key)
91
92 return key
93
94 @property
95 def indent_str(self):
96 return self.indentation_char * (self.indentation_level * self.indent)
diff --git a/lib/python/qmk/keymap.py b/lib/python/qmk/keymap.py
index 266532f50..d8495c38b 100644
--- a/lib/python/qmk/keymap.py
+++ b/lib/python/qmk/keymap.py
@@ -42,7 +42,7 @@ def template_json(keyboard):
42 template_file = Path('keyboards/%s/templates/keymap.json' % keyboard) 42 template_file = Path('keyboards/%s/templates/keymap.json' % keyboard)
43 template = {'keyboard': keyboard} 43 template = {'keyboard': keyboard}
44 if template_file.exists(): 44 if template_file.exists():
45 template.update(json.loads(template_file.read_text())) 45 template.update(json.load(template_file.open(encoding='utf-8')))
46 46
47 return template 47 return template
48 48
@@ -58,7 +58,7 @@ def template_c(keyboard):
58 """ 58 """
59 template_file = Path('keyboards/%s/templates/keymap.c' % keyboard) 59 template_file = Path('keyboards/%s/templates/keymap.c' % keyboard)
60 if template_file.exists(): 60 if template_file.exists():
61 template = template_file.read_text() 61 template = template_file.read_text(encoding='utf-8')
62 else: 62 else:
63 template = DEFAULT_KEYMAP_C 63 template = DEFAULT_KEYMAP_C
64 64
@@ -469,7 +469,7 @@ def parse_keymap_c(keymap_file, use_cpp=True):
469 if use_cpp: 469 if use_cpp:
470 keymap_file = _c_preprocess(keymap_file) 470 keymap_file = _c_preprocess(keymap_file)
471 else: 471 else:
472 keymap_file = keymap_file.read_text() 472 keymap_file = keymap_file.read_text(encoding='utf-8')
473 473
474 keymap = dict() 474 keymap = dict()
475 keymap['layers'] = _get_layers(keymap_file) 475 keymap['layers'] = _get_layers(keymap_file)
diff --git a/lib/python/qmk/os_helpers/linux/__init__.py b/lib/python/qmk/os_helpers/linux/__init__.py
index 86850bf28..9e73964e4 100644
--- a/lib/python/qmk/os_helpers/linux/__init__.py
+++ b/lib/python/qmk/os_helpers/linux/__init__.py
@@ -48,6 +48,7 @@ def check_udev_rules():
48 _udev_rule("03eb", "2ff3"), # ATmega16U4 48 _udev_rule("03eb", "2ff3"), # ATmega16U4
49 _udev_rule("03eb", "2ff4"), # ATmega32U4 49 _udev_rule("03eb", "2ff4"), # ATmega32U4
50 _udev_rule("03eb", "2ff9"), # AT90USB64 50 _udev_rule("03eb", "2ff9"), # AT90USB64
51 _udev_rule("03eb", "2ffa"), # AT90USB162
51 _udev_rule("03eb", "2ffb") # AT90USB128 52 _udev_rule("03eb", "2ffb") # AT90USB128
52 }, 53 },
53 'kiibohd': {_udev_rule("1c11", "b007")}, 54 'kiibohd': {_udev_rule("1c11", "b007")},
@@ -94,7 +95,7 @@ def check_udev_rules():
94 95
95 # Collect all rules from the config files 96 # Collect all rules from the config files
96 for rule_file in udev_rules: 97 for rule_file in udev_rules:
97 for line in rule_file.read_text().split('\n'): 98 for line in rule_file.read_text(encoding='utf-8').split('\n'):
98 line = line.strip() 99 line = line.strip()
99 if not line.startswith("#") and len(line): 100 if not line.startswith("#") and len(line):
100 current_rules.add(line) 101 current_rules.add(line)
diff --git a/lib/python/qmk/tests/test_cli_commands.py b/lib/python/qmk/tests/test_cli_commands.py
index f889833d0..82c42a20e 100644
--- a/lib/python/qmk/tests/test_cli_commands.py
+++ b/lib/python/qmk/tests/test_cli_commands.py
@@ -16,7 +16,7 @@ def check_subcommand(command, *args):
16def check_subcommand_stdin(file_to_read, command, *args): 16def check_subcommand_stdin(file_to_read, command, *args):
17 """Pipe content of a file to a command and return output. 17 """Pipe content of a file to a command and return output.
18 """ 18 """
19 with open(file_to_read) as my_file: 19 with open(file_to_read, encoding='utf-8') as my_file:
20 cmd = ['bin/qmk', command, *args] 20 cmd = ['bin/qmk', command, *args]
21 result = run(cmd, stdin=my_file, stdout=PIPE, stderr=STDOUT, universal_newlines=True) 21 result = run(cmd, stdin=my_file, stdout=PIPE, stderr=STDOUT, universal_newlines=True)
22 return result 22 return result
@@ -230,3 +230,32 @@ def test_generate_rgb_breathe_table():
230 check_returncode(result) 230 check_returncode(result)
231 assert 'Breathing center: 1.2' in result.stdout 231 assert 'Breathing center: 1.2' in result.stdout
232 assert 'Breathing max: 127' in result.stdout 232 assert 'Breathing max: 127' in result.stdout
233
234
235def test_generate_config_h():
236 result = check_subcommand('generate-config-h', '-kb', 'handwired/pytest/basic')
237 check_returncode(result)
238 assert '# define DEVICE_VER 0x0001' in result.stdout
239 assert '# define DESCRIPTION handwired/pytest/basic' in result.stdout
240 assert '# define DIODE_DIRECTION COL2ROW' in result.stdout
241 assert '# define MANUFACTURER none' in result.stdout
242 assert '# define PRODUCT handwired/pytest/basic' in result.stdout
243 assert '# define PRODUCT_ID 0x6465' in result.stdout
244 assert '# define VENDOR_ID 0xFEED' in result.stdout
245 assert '# define MATRIX_COLS 1' in result.stdout
246 assert '# define MATRIX_COL_PINS { F4 }' in result.stdout
247 assert '# define MATRIX_ROWS 1' in result.stdout
248 assert '# define MATRIX_ROW_PINS { F5 }' in result.stdout
249
250
251def test_generate_rules_mk():
252 result = check_subcommand('generate-rules-mk', '-kb', 'handwired/pytest/basic')
253 check_returncode(result)
254 assert 'BOOTLOADER ?= atmel-dfu' in result.stdout
255 assert 'MCU ?= atmega32u4' in result.stdout
256
257
258def test_generate_layouts():
259 result = check_subcommand('generate-layouts', '-kb', 'handwired/pytest/basic')
260 check_returncode(result)
261 assert '#define LAYOUT_custom(k0A) {' in result.stdout