aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZach White <skullydazed@users.noreply.github.com>2020-05-26 13:05:41 -0700
committerGitHub <noreply@github.com>2020-05-26 13:05:41 -0700
commit751316c34465ea77e066c3052729b207f3d62e0c (patch)
treecb99656b93c156757e2fd7c84fe716f9c300ca89
parent5d3bf8a050f3c0beb1f91147dc1ab54de36cbb05 (diff)
downloadqmk_firmware-751316c34465ea77e066c3052729b207f3d62e0c.tar.gz
qmk_firmware-751316c34465ea77e066c3052729b207f3d62e0c.zip
[CLI] Add a subcommand for getting information about a keyboard (#8666)
You can now use `qmk info` to get information about keyboards and keymaps. Co-authored-by: Erovia <Erovia@users.noreply.github.com>
-rw-r--r--docs/cli_commands.md37
-rw-r--r--lib/python/qmk/c_parse.py161
-rw-r--r--lib/python/qmk/cli/__init__.py1
-rw-r--r--lib/python/qmk/cli/cformat.py10
-rwxr-xr-xlib/python/qmk/cli/info.py141
-rw-r--r--lib/python/qmk/cli/list/keymaps.py18
-rw-r--r--lib/python/qmk/commands.py1
-rw-r--r--lib/python/qmk/comment_remover.py20
-rw-r--r--lib/python/qmk/constants.py6
-rw-r--r--lib/python/qmk/decorators.py9
-rw-r--r--lib/python/qmk/info.py249
-rw-r--r--lib/python/qmk/keyboard.py111
-rw-r--r--lib/python/qmk/keymap.py75
-rw-r--r--lib/python/qmk/makefile.py32
-rw-r--r--lib/python/qmk/math.py33
-rw-r--r--lib/python/qmk/path.py26
-rw-r--r--lib/python/qmk/tests/test_cli_commands.py104
17 files changed, 921 insertions, 113 deletions
diff --git a/docs/cli_commands.md b/docs/cli_commands.md
index eff5321bd..bb0de3c0d 100644
--- a/docs/cli_commands.md
+++ b/docs/cli_commands.md
@@ -6,6 +6,8 @@
6 6
7This command allows you to compile firmware from any directory. You can compile JSON exports from <https://config.qmk.fm>, compile keymaps in the repo, or compile the keyboard in the current working directory. 7This command allows you to compile firmware from any directory. You can compile JSON exports from <https://config.qmk.fm>, compile keymaps in the repo, or compile the keyboard in the current working directory.
8 8
9This command is directory aware. It will automatically fill in KEYBOARD and/or KEYMAP if you are in a keyboard or keymap directory.
10
9**Usage for Configurator Exports**: 11**Usage for Configurator Exports**:
10 12
11``` 13```
@@ -73,8 +75,9 @@ $ qmk compile -kb dz60
73 75
74## `qmk flash` 76## `qmk flash`
75 77
76This command is similar to `qmk compile`, but can also target a bootloader. The bootloader is optional, and is set to `:flash` by default. 78This command is similar to `qmk compile`, but can also target a bootloader. The bootloader is optional, and is set to `:flash` by default. To specify a different bootloader, use `-bl <bootloader>`. Visit the [Flashing Firmware](flashing.md) guide for more details of the available bootloaders.
77To specify a different bootloader, use `-bl <bootloader>`. Visit the [Flashing Firmware](flashing.md) guide for more details of the available bootloaders. 79
80This command is directory aware. It will automatically fill in KEYBOARD and/or KEYMAP if you are in a keyboard or keymap directory.
78 81
79**Usage for Configurator Exports**: 82**Usage for Configurator Exports**:
80 83
@@ -128,6 +131,32 @@ Check your environment and report problems only:
128 131
129 qmk doctor -n 132 qmk doctor -n
130 133
134## `qmk info`
135
136Displays information about keyboards and keymaps in QMK. You can use this to get information about a keyboard, show the layouts, display the underlying key matrix, or to pretty-print JSON keymaps.
137
138**Usage**:
139
140```
141qmk info [-f FORMAT] [-m] [-l] [-km KEYMAP] [-kb KEYBOARD]
142```
143
144This command is directory aware. It will automatically fill in KEYBOARD and/or KEYMAP if you are in a keyboard or keymap directory.
145
146**Examples**:
147
148Show basic information for a keyboard:
149
150 qmk info -kb planck/rev5
151
152Show the matrix for a keyboard:
153
154 qmk info -kb ergodox_ez -m
155
156Show a JSON keymap for a keyboard:
157
158 qmk info -kb clueboard/california -km default
159
131## `qmk json2c` 160## `qmk json2c`
132 161
133Creates a keymap.c from a QMK Configurator export. 162Creates a keymap.c from a QMK Configurator export.
@@ -152,6 +181,8 @@ qmk list-keyboards
152 181
153This command lists all the keymaps for a specified keyboard (and revision). 182This command lists all the keymaps for a specified keyboard (and revision).
154 183
184This command is directory aware. It will automatically fill in KEYBOARD if you are in a keyboard directory.
185
155**Usage**: 186**Usage**:
156 187
157``` 188```
@@ -162,6 +193,8 @@ qmk list-keymaps -kb planck/ez
162 193
163This command creates a new keymap based on a keyboard's existing default keymap. 194This command creates a new keymap based on a keyboard's existing default keymap.
164 195
196This command is directory aware. It will automatically fill in KEYBOARD and/or KEYMAP if you are in a keyboard or keymap directory.
197
165**Usage**: 198**Usage**:
166 199
167``` 200```
diff --git a/lib/python/qmk/c_parse.py b/lib/python/qmk/c_parse.py
new file mode 100644
index 000000000..e41e271a4
--- /dev/null
+++ b/lib/python/qmk/c_parse.py
@@ -0,0 +1,161 @@
1"""Functions for working with config.h files.
2"""
3from pathlib import Path
4
5from milc import cli
6
7from qmk.comment_remover import comment_remover
8
9default_key_entry = {'x': -1, 'y': 0, 'w': 1}
10
11
12def c_source_files(dir_names):
13 """Returns a list of all *.c, *.h, and *.cpp files for a given list of directories
14
15 Args:
16
17 dir_names
18 List of directories relative to `qmk_firmware`.
19 """
20 files = []
21 for dir in dir_names:
22 files.extend(file for file in Path(dir).glob('**/*') if file.suffix in ['.c', '.h', '.cpp'])
23 return files
24
25
26def find_layouts(file):
27 """Returns list of parsed LAYOUT preprocessor macros found in the supplied include file.
28 """
29 file = Path(file)
30 aliases = {} # Populated with all `#define`s that aren't functions
31 parsed_layouts = {}
32
33 # Search the file for LAYOUT macros and aliases
34 file_contents = file.read_text()
35 file_contents = comment_remover(file_contents)
36 file_contents = file_contents.replace('\\\n', '')
37
38 for line in file_contents.split('\n'):
39 if line.startswith('#define') and '(' in line and 'LAYOUT' in line:
40 # We've found a LAYOUT macro
41 macro_name, layout, matrix = _parse_layout_macro(line.strip())
42
43 # Reject bad macro names
44 if macro_name.startswith('LAYOUT_kc') or not macro_name.startswith('LAYOUT'):
45 continue
46
47 # Parse the matrix data
48 matrix_locations = _parse_matrix_locations(matrix, file, macro_name)
49
50 # Parse the layout entries into a basic structure
51 default_key_entry['x'] = -1 # Set to -1 so _default_key(key) will increment it to 0
52 layout = layout.strip()
53 parsed_layout = [_default_key(key) for key in layout.split(',')]
54
55 for key in parsed_layout:
56 key['matrix'] = matrix_locations.get(key['label'])
57
58 parsed_layouts[macro_name] = {
59 'key_count': len(parsed_layout),
60 'layout': parsed_layout,
61 'filename': str(file),
62 }
63
64 elif '#define' in line:
65 # Attempt to extract a new layout alias
66 try:
67 _, pp_macro_name, pp_macro_text = line.strip().split(' ', 2)
68 aliases[pp_macro_name] = pp_macro_text
69 except ValueError:
70 continue
71
72 # Populate our 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
79
80def parse_config_h_file(config_h_file, config_h=None):
81 """Extract defines from a config.h file.
82 """
83 if not config_h:
84 config_h = {}
85
86 config_h_file = Path(config_h_file)
87
88 if config_h_file.exists():
89 config_h_text = config_h_file.read_text()
90 config_h_text = config_h_text.replace('\\\n', '')
91
92 for linenum, line in enumerate(config_h_text.split('\n')):
93 line = line.strip()
94
95 if '//' in line:
96 line = line[:line.index('//')].strip()
97
98 if not line:
99 continue
100
101 line = line.split()
102
103 if line[0] == '#define':
104 if len(line) == 1:
105 cli.log.error('%s: Incomplete #define! On or around line %s' % (config_h_file, linenum))
106 elif len(line) == 2:
107 config_h[line[1]] = True
108 else:
109 config_h[line[1]] = ' '.join(line[2:])
110
111 elif line[0] == '#undef':
112 if len(line) == 2:
113 if line[1] in config_h:
114 if config_h[line[1]] is True:
115 del config_h[line[1]]
116 else:
117 config_h[line[1]] = False
118 else:
119 cli.log.error('%s: Incomplete #undef! On or around line %s' % (config_h_file, linenum))
120
121 return config_h
122
123
124def _default_key(label=None):
125 """Increment x and return a copy of the default_key_entry.
126 """
127 default_key_entry['x'] += 1
128 new_key = default_key_entry.copy()
129
130 if label:
131 new_key['label'] = label
132
133 return new_key
134
135
136def _parse_layout_macro(layout_macro):
137 """Split the LAYOUT macro into its constituent parts
138 """
139 layout_macro = layout_macro.replace('\\', '').replace(' ', '').replace('\t', '').replace('#define', '')
140 macro_name, layout = layout_macro.split('(', 1)
141 layout, matrix = layout.split(')', 1)
142
143 return macro_name, layout, matrix
144
145
146def _parse_matrix_locations(matrix, file, macro_name):
147 """Parse raw matrix data into a dictionary keyed by the LAYOUT identifier.
148 """
149 matrix_locations = {}
150
151 for row_num, row in enumerate(matrix.split('},{')):
152 if row.startswith('LAYOUT'):
153 cli.log.error('%s: %s: Nested layout macro detected. Matrix data not available!', file, macro_name)
154 break
155
156 row = row.replace('{', '').replace('}', '')
157 for col_num, identifier in enumerate(row.split(',')):
158 if identifier != 'KC_NO':
159 matrix_locations[identifier] = (row_num, col_num)
160
161 return matrix_locations
diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py
index 394a1353b..47f60c601 100644
--- a/lib/python/qmk/cli/__init__.py
+++ b/lib/python/qmk/cli/__init__.py
@@ -13,6 +13,7 @@ from . import docs
13from . import doctor 13from . import doctor
14from . import flash 14from . import flash
15from . import hello 15from . import hello
16from . import info
16from . import json 17from . import json
17from . import json2c 18from . import json2c
18from . import list 19from . import list
diff --git a/lib/python/qmk/cli/cformat.py b/lib/python/qmk/cli/cformat.py
index 0cd8b6192..600161c5c 100644
--- a/lib/python/qmk/cli/cformat.py
+++ b/lib/python/qmk/cli/cformat.py
@@ -4,7 +4,9 @@ import subprocess
4from shutil import which 4from shutil import which
5 5
6from milc import cli 6from milc import cli
7import qmk.path 7
8from qmk.path import normpath
9from qmk.c_parse import c_source_files
8 10
9 11
10def cformat_run(files, all_files): 12def cformat_run(files, all_files):
@@ -45,10 +47,10 @@ def cformat(cli):
45 ignores = ['tmk_core/protocol/usb_hid', 'quantum/template'] 47 ignores = ['tmk_core/protocol/usb_hid', 'quantum/template']
46 # Find the list of files to format 48 # Find the list of files to format
47 if cli.args.files: 49 if cli.args.files:
48 files.extend(qmk.path.normpath(file) for file in cli.args.files) 50 files.extend(normpath(file) for file in cli.args.files)
49 # If -a is specified 51 # If -a is specified
50 elif cli.args.all_files: 52 elif cli.args.all_files:
51 all_files = qmk.path.c_source_files(core_dirs) 53 all_files = c_source_files(core_dirs)
52 # The following statement checks each file to see if the file path is in the ignored directories. 54 # The following statement checks each file to see if the file path is in the ignored directories.
53 files.extend(file for file in all_files if not any(i in str(file) for i in ignores)) 55 files.extend(file for file in all_files if not any(i in str(file) for i in ignores))
54 # No files specified & no -a flag 56 # No files specified & no -a flag
@@ -56,7 +58,7 @@ def cformat(cli):
56 base_args = ['git', 'diff', '--name-only', cli.args.base_branch] 58 base_args = ['git', 'diff', '--name-only', cli.args.base_branch]
57 out = subprocess.run(base_args + core_dirs, check=True, stdout=subprocess.PIPE) 59 out = subprocess.run(base_args + core_dirs, check=True, stdout=subprocess.PIPE)
58 changed_files = filter(None, out.stdout.decode('UTF-8').split('\n')) 60 changed_files = filter(None, out.stdout.decode('UTF-8').split('\n'))
59 filtered_files = [qmk.path.normpath(file) for file in changed_files if not any(i in file for i in ignores)] 61 filtered_files = [normpath(file) for file in changed_files if not any(i in file for i in ignores)]
60 files.extend(file for file in filtered_files if file.exists() and file.suffix in ['.c', '.h', '.cpp']) 62 files.extend(file for file in filtered_files if file.exists() and file.suffix in ['.c', '.h', '.cpp'])
61 63
62 # Run clang-format on the files we've found 64 # Run clang-format on the files we've found
diff --git a/lib/python/qmk/cli/info.py b/lib/python/qmk/cli/info.py
new file mode 100755
index 000000000..6977673e2
--- /dev/null
+++ b/lib/python/qmk/cli/info.py
@@ -0,0 +1,141 @@
1"""Keyboard information script.
2
3Compile an info.json for a particular keyboard and pretty-print it.
4"""
5import json
6
7from milc import cli
8
9from qmk.decorators import automagic_keyboard, automagic_keymap
10from qmk.keyboard import render_layouts, render_layout
11from qmk.keymap import locate_keymap
12from qmk.info import info_json
13from qmk.path import is_keyboard
14
15ROW_LETTERS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop'
16COL_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijilmnopqrstuvwxyz'
17
18
19def show_keymap(info_json, title_caps=True):
20 """Render the keymap in ascii art.
21 """
22 keymap_path = locate_keymap(cli.config.info.keyboard, cli.config.info.keymap)
23
24 if keymap_path and keymap_path.suffix == '.json':
25 if title_caps:
26 cli.echo('{fg_blue}Keymap "%s"{fg_reset}:', cli.config.info.keymap)
27 else:
28 cli.echo('{fg_blue}keymap_%s{fg_reset}:', cli.config.info.keymap)
29
30 keymap_data = json.load(keymap_path.open())
31 layout_name = keymap_data['layout']
32
33 for layer_num, layer in enumerate(keymap_data['layers']):
34 if title_caps:
35 cli.echo('{fg_cyan}Layer %s{fg_reset}:', layer_num)
36 else:
37 cli.echo('{fg_cyan}layer_%s{fg_reset}:', layer_num)
38
39 print(render_layout(info_json['layouts'][layout_name]['layout'], layer))
40
41
42def show_layouts(kb_info_json, title_caps=True):
43 """Render the layouts with info.json labels.
44 """
45 for layout_name, layout_art in render_layouts(kb_info_json).items():
46 title = layout_name.title() if title_caps else layout_name
47 cli.echo('{fg_cyan}%s{fg_reset}:', title)
48 print(layout_art) # Avoid passing dirty data to cli.echo()
49
50
51def show_matrix(info_json, title_caps=True):
52 """Render the layout with matrix labels in ascii art.
53 """
54 for layout_name, layout in info_json['layouts'].items():
55 # Build our label list
56 labels = []
57 for key in layout['layout']:
58 if key['matrix']:
59 row = ROW_LETTERS[key['matrix'][0]]
60 col = COL_LETTERS[key['matrix'][1]]
61
62 labels.append(row + col)
63 else:
64 labels.append('')
65
66 # Print the header
67 if title_caps:
68 cli.echo('{fg_blue}Matrix for "%s"{fg_reset}:', layout_name)
69 else:
70 cli.echo('{fg_blue}matrix_%s{fg_reset}:', layout_name)
71
72 print(render_layout(info_json['layouts'][layout_name]['layout'], labels))
73
74
75@cli.argument('-kb', '--keyboard', help='Keyboard to show info for.')
76@cli.argument('-km', '--keymap', help='Show the layers for a JSON keymap too.')
77@cli.argument('-l', '--layouts', action='store_true', help='Render the layouts.')
78@cli.argument('-m', '--matrix', action='store_true', help='Render the layouts with matrix information.')
79@cli.argument('-f', '--format', default='friendly', arg_only=True, help='Format to display the data in (friendly, text, json) (Default: friendly).')
80@cli.subcommand('Keyboard information.')
81@automagic_keyboard
82@automagic_keymap
83def info(cli):
84 """Compile an info.json for a particular keyboard and pretty-print it.
85 """
86 # Determine our keyboard(s)
87 if not is_keyboard(cli.config.info.keyboard):
88 cli.log.error('Invalid keyboard: %s!', cli.config.info.keyboard)
89 exit(1)
90
91 # Build the info.json file
92 kb_info_json = info_json(cli.config.info.keyboard)
93
94 # Output in the requested format
95 if cli.args.format == 'json':
96 print(json.dumps(kb_info_json))
97 exit()
98
99 if cli.args.format == 'text':
100 for key in sorted(kb_info_json):
101 if key == 'layouts':
102 cli.echo('{fg_blue}layouts{fg_reset}: %s', ', '.join(sorted(kb_info_json['layouts'].keys())))
103 else:
104 cli.echo('{fg_blue}%s{fg_reset}: %s', key, kb_info_json[key])
105
106 if cli.config.info.layouts:
107 show_layouts(kb_info_json, False)
108
109 if cli.config.info.matrix:
110 show_matrix(kb_info_json, False)
111
112 if cli.config_source.info.keymap and cli.config_source.info.keymap != 'config_file':
113 show_keymap(kb_info_json, False)
114
115 elif cli.args.format == 'friendly':
116 cli.echo('{fg_blue}Keyboard Name{fg_reset}: %s', kb_info_json.get('keyboard_name', 'Unknown'))
117 cli.echo('{fg_blue}Manufacturer{fg_reset}: %s', kb_info_json.get('manufacturer', 'Unknown'))
118 if 'url' in kb_info_json:
119 cli.echo('{fg_blue}Website{fg_reset}: %s', kb_info_json['url'])
120 if kb_info_json.get('maintainer') == 'qmk':
121 cli.echo('{fg_blue}Maintainer{fg_reset}: QMK Community')
122 else:
123 cli.echo('{fg_blue}Maintainer{fg_reset}: %s', kb_info_json.get('maintainer', 'qmk'))
124 cli.echo('{fg_blue}Keyboard Folder{fg_reset}: %s', kb_info_json.get('keyboard_folder', 'Unknown'))
125 cli.echo('{fg_blue}Layouts{fg_reset}: %s', ', '.join(sorted(kb_info_json['layouts'].keys())))
126 if 'width' in kb_info_json and 'height' in kb_info_json:
127 cli.echo('{fg_blue}Size{fg_reset}: %s x %s' % (kb_info_json['width'], kb_info_json['height']))
128 cli.echo('{fg_blue}Processor{fg_reset}: %s', kb_info_json.get('processor', 'Unknown'))
129 cli.echo('{fg_blue}Bootloader{fg_reset}: %s', kb_info_json.get('bootloader', 'Unknown'))
130
131 if cli.config.info.layouts:
132 show_layouts(kb_info_json, True)
133
134 if cli.config.info.matrix:
135 show_matrix(kb_info_json, True)
136
137 if cli.config_source.info.keymap and cli.config_source.info.keymap != 'config_file':
138 show_keymap(kb_info_json, True)
139
140 else:
141 cli.log.error('Unknown format: %s', cli.args.format)
diff --git a/lib/python/qmk/cli/list/keymaps.py b/lib/python/qmk/cli/list/keymaps.py
index cec9ca022..b18289eb3 100644
--- a/lib/python/qmk/cli/list/keymaps.py
+++ b/lib/python/qmk/cli/list/keymaps.py
@@ -4,7 +4,7 @@ from milc import cli
4 4
5import qmk.keymap 5import qmk.keymap
6from qmk.decorators import automagic_keyboard 6from qmk.decorators import automagic_keyboard
7from qmk.errors import NoSuchKeyboardError 7from qmk.path import is_keyboard
8 8
9 9
10@cli.argument("-kb", "--keyboard", help="Specify keyboard name. Example: 1upkeyboards/1up60hse") 10@cli.argument("-kb", "--keyboard", help="Specify keyboard name. Example: 1upkeyboards/1up60hse")
@@ -13,13 +13,9 @@ from qmk.errors import NoSuchKeyboardError
13def list_keymaps(cli): 13def list_keymaps(cli):
14 """List the keymaps for a specific keyboard 14 """List the keymaps for a specific keyboard
15 """ 15 """
16 try: 16 if not is_keyboard(cli.config.list_keymaps.keyboard):
17 for name in qmk.keymap.list_keymaps(cli.config.list_keymaps.keyboard): 17 cli.log.error('Keyboard %s does not exist!', cli.config.list_keymaps.keyboard)
18 # We echo instead of cli.log.info to allow easier piping of this output 18 exit(1)
19 cli.echo('%s', name) 19
20 except NoSuchKeyboardError as e: 20 for name in qmk.keymap.list_keymaps(cli.config.list_keymaps.keyboard):
21 cli.echo("{fg_red}%s: %s", cli.config.list_keymaps.keyboard, e.message) 21 print(name)
22 except (FileNotFoundError, PermissionError) as e:
23 cli.echo("{fg_red}%s: %s", cli.config.list_keymaps.keyboard, e)
24 except TypeError:
25 cli.echo("{fg_red}Something went wrong. Did you specify a keyboard?")
diff --git a/lib/python/qmk/commands.py b/lib/python/qmk/commands.py
index 5d2a03c9a..5a6e60988 100644
--- a/lib/python/qmk/commands.py
+++ b/lib/python/qmk/commands.py
@@ -64,6 +64,7 @@ def compile_configurator_json(user_keymap, bootloader=None):
64def parse_configurator_json(configurator_file): 64def parse_configurator_json(configurator_file):
65 """Open and parse a configurator json export 65 """Open and parse a configurator json export
66 """ 66 """
67 # FIXME(skullydazed/anyone): Add validation here
67 user_keymap = json.load(configurator_file) 68 user_keymap = json.load(configurator_file)
68 69
69 return user_keymap 70 return user_keymap
diff --git a/lib/python/qmk/comment_remover.py b/lib/python/qmk/comment_remover.py
new file mode 100644
index 000000000..45a25257f
--- /dev/null
+++ b/lib/python/qmk/comment_remover.py
@@ -0,0 +1,20 @@
1"""Removes C/C++ style comments from text.
2
3Gratefully adapted from https://stackoverflow.com/a/241506
4"""
5import re
6
7comment_pattern = re.compile(r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', re.DOTALL | re.MULTILINE)
8
9
10def _comment_stripper(match):
11 """Removes C/C++ style comments from a regex match.
12 """
13 s = match.group(0)
14 return ' ' if s.startswith('/') else s
15
16
17def comment_remover(text):
18 """Remove C/C++ style comments from text.
19 """
20 return re.sub(comment_pattern, _comment_stripper, text)
diff --git a/lib/python/qmk/constants.py b/lib/python/qmk/constants.py
index 3e4709969..f0d56c443 100644
--- a/lib/python/qmk/constants.py
+++ b/lib/python/qmk/constants.py
@@ -7,3 +7,9 @@ QMK_FIRMWARE = Path.cwd()
7 7
8# This is the number of directories under `qmk_firmware/keyboards` that will be traversed. This is currently a limitation of our make system. 8# This is the number of directories under `qmk_firmware/keyboards` that will be traversed. This is currently a limitation of our make system.
9MAX_KEYBOARD_SUBFOLDERS = 5 9MAX_KEYBOARD_SUBFOLDERS = 5
10
11# Supported processor types
12ARM_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303'
13AVR_PROCESSORS = 'at90usb1286', 'at90usb646', 'atmega16u2', 'atmega328p', 'atmega32a', 'atmega32u2', 'atmega32u4', None
14ALL_PROCESSORS = ARM_PROCESSORS + AVR_PROCESSORS
15VUSB_PROCESSORS = 'atmega328p', 'atmega32a'
diff --git a/lib/python/qmk/decorators.py b/lib/python/qmk/decorators.py
index 94e14bf37..f8f2facb1 100644
--- a/lib/python/qmk/decorators.py
+++ b/lib/python/qmk/decorators.py
@@ -5,7 +5,8 @@ from pathlib import Path
5 5
6from milc import cli 6from milc import cli
7 7
8from qmk.path import is_keyboard, is_keymap_dir, under_qmk_firmware 8from qmk.keymap import is_keymap_dir
9from qmk.path import is_keyboard, under_qmk_firmware
9 10
10 11
11def automagic_keyboard(func): 12def automagic_keyboard(func):
@@ -67,18 +68,18 @@ def automagic_keymap(func):
67 while current_path.parent.name != 'keymaps': 68 while current_path.parent.name != 'keymaps':
68 current_path = current_path.parent 69 current_path = current_path.parent
69 cli.config[cli._entrypoint.__name__]['keymap'] = current_path.name 70 cli.config[cli._entrypoint.__name__]['keymap'] = current_path.name
70 cli.config_source[cli._entrypoint.__name__]['keyboard'] = 'keymap_directory' 71 cli.config_source[cli._entrypoint.__name__]['keymap'] = 'keymap_directory'
71 72
72 # If we're in `qmk_firmware/layouts` guess the name from the community keymap they're in 73 # If we're in `qmk_firmware/layouts` guess the name from the community keymap they're in
73 elif relative_cwd.parts[0] == 'layouts' and is_keymap_dir(relative_cwd): 74 elif relative_cwd.parts[0] == 'layouts' and is_keymap_dir(relative_cwd):
74 cli.config[cli._entrypoint.__name__]['keymap'] = relative_cwd.name 75 cli.config[cli._entrypoint.__name__]['keymap'] = relative_cwd.name
75 cli.config_source[cli._entrypoint.__name__]['keyboard'] = 'layouts_directory' 76 cli.config_source[cli._entrypoint.__name__]['keymap'] = 'layouts_directory'
76 77
77 # If we're in `qmk_firmware/users` guess the name from the userspace they're in 78 # If we're in `qmk_firmware/users` guess the name from the userspace they're in
78 elif relative_cwd.parts[0] == 'users': 79 elif relative_cwd.parts[0] == 'users':
79 # Guess the keymap name based on which userspace they're in 80 # Guess the keymap name based on which userspace they're in
80 cli.config[cli._entrypoint.__name__]['keymap'] = relative_cwd.parts[1] 81 cli.config[cli._entrypoint.__name__]['keymap'] = relative_cwd.parts[1]
81 cli.config_source[cli._entrypoint.__name__]['keyboard'] = 'users_directory' 82 cli.config_source[cli._entrypoint.__name__]['keymap'] = 'users_directory'
82 83
83 return func(*args, **kwargs) 84 return func(*args, **kwargs)
84 85
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
new file mode 100644
index 000000000..e1ace5d51
--- /dev/null
+++ b/lib/python/qmk/info.py
@@ -0,0 +1,249 @@
1"""Functions that help us generate and use info.json files.
2"""
3import json
4from glob import glob
5from pathlib import Path
6
7from milc import cli
8
9from qmk.constants import ARM_PROCESSORS, AVR_PROCESSORS, VUSB_PROCESSORS
10from qmk.c_parse import find_layouts
11from qmk.keyboard import config_h, rules_mk
12from qmk.math import compute
13
14
15def info_json(keyboard):
16 """Generate the info.json data for a specific keyboard.
17 """
18 info_data = {
19 'keyboard_name': str(keyboard),
20 'keyboard_folder': str(keyboard),
21 'layouts': {},
22 'maintainer': 'qmk',
23 }
24
25 for layout_name, layout_json in _find_all_layouts(keyboard).items():
26 if not layout_name.startswith('LAYOUT_kc'):
27 info_data['layouts'][layout_name] = layout_json
28
29 info_data = merge_info_jsons(keyboard, info_data)
30 info_data = _extract_config_h(info_data)
31 info_data = _extract_rules_mk(info_data)
32
33 return info_data
34
35
36def _extract_config_h(info_data):
37 """Pull some keyboard information from existing rules.mk files
38 """
39 config_c = config_h(info_data['keyboard_folder'])
40 row_pins = config_c.get('MATRIX_ROW_PINS', '').replace('{', '').replace('}', '').strip()
41 col_pins = config_c.get('MATRIX_COL_PINS', '').replace('{', '').replace('}', '').strip()
42 direct_pins = config_c.get('DIRECT_PINS', '').replace(' ', '')[1:-1]
43
44 info_data['diode_direction'] = config_c.get('DIODE_DIRECTION')
45 info_data['matrix_size'] = {
46 'rows': compute(config_c.get('MATRIX_ROWS', '0')),
47 'cols': compute(config_c.get('MATRIX_COLS', '0')),
48 }
49 info_data['matrix_pins'] = {}
50
51 if row_pins:
52 info_data['matrix_pins']['rows'] = row_pins.split(',')
53 if col_pins:
54 info_data['matrix_pins']['cols'] = col_pins.split(',')
55
56 if direct_pins:
57 direct_pin_array = []
58 for row in direct_pins.split('},{'):
59 if row.startswith('{'):
60 row = row[1:]
61 if row.endswith('}'):
62 row = row[:-1]
63
64 direct_pin_array.append([])
65
66 for pin in row.split(','):
67 if pin == 'NO_PIN':
68 pin = None
69
70 direct_pin_array[-1].append(pin)
71
72 info_data['matrix_pins']['direct'] = direct_pin_array
73
74 info_data['usb'] = {
75 'vid': config_c.get('VENDOR_ID'),
76 'pid': config_c.get('PRODUCT_ID'),
77 'device_ver': config_c.get('DEVICE_VER'),
78 'manufacturer': config_c.get('MANUFACTURER'),
79 'product': config_c.get('PRODUCT'),
80 'description': config_c.get('DESCRIPTION'),
81 }
82
83 return info_data
84
85
86def _extract_rules_mk(info_data):
87 """Pull some keyboard information from existing rules.mk files
88 """
89 rules = rules_mk(info_data['keyboard_folder'])
90 mcu = rules.get('MCU')
91
92 if mcu in ARM_PROCESSORS:
93 arm_processor_rules(info_data, rules)
94 elif mcu in AVR_PROCESSORS:
95 avr_processor_rules(info_data, rules)
96 else:
97 cli.log.warning("%s: Unknown MCU: %s" % (info_data['keyboard_folder'], mcu))
98 unknown_processor_rules(info_data, rules)
99
100 return info_data
101
102
103def _find_all_layouts(keyboard):
104 """Looks for layout macros associated with this keyboard.
105 """
106 layouts = {}
107 rules = rules_mk(keyboard)
108 keyboard_path = Path(rules.get('DEFAULT_FOLDER', keyboard))
109
110 # Pull in all layouts defined in the standard files
111 current_path = Path('keyboards/')
112 for directory in keyboard_path.parts:
113 current_path = current_path / directory
114 keyboard_h = '%s.h' % (directory,)
115 keyboard_h_path = current_path / keyboard_h
116 if keyboard_h_path.exists():
117 layouts.update(find_layouts(keyboard_h_path))
118
119 if not layouts:
120 # If we didn't find any layouts above we widen our search. This is error
121 # prone which is why we want to encourage people to follow the standard above.
122 cli.log.warning('%s: Falling back to searching for KEYMAP/LAYOUT macros.' % (keyboard))
123 for file in glob('keyboards/%s/*.h' % keyboard):
124 if file.endswith('.h'):
125 these_layouts = find_layouts(file)
126 if these_layouts:
127 layouts.update(these_layouts)
128
129 if 'LAYOUTS' in rules:
130 # Match these up against the supplied layouts
131 supported_layouts = rules['LAYOUTS'].strip().split()
132 for layout_name in sorted(layouts):
133 if not layout_name.startswith('LAYOUT_'):
134 continue
135 layout_name = layout_name[7:]
136 if layout_name in supported_layouts:
137 supported_layouts.remove(layout_name)
138
139 if supported_layouts:
140 cli.log.error('%s: Missing LAYOUT() macro for %s' % (keyboard, ', '.join(supported_layouts)))
141
142 return layouts
143
144
145def arm_processor_rules(info_data, rules):
146 """Setup the default info for an ARM board.
147 """
148 info_data['processor_type'] = 'arm'
149 info_data['bootloader'] = rules['BOOTLOADER'] if 'BOOTLOADER' in rules else 'unknown'
150 info_data['processor'] = rules['MCU'] if 'MCU' in rules else 'unknown'
151 info_data['protocol'] = 'ChibiOS'
152
153 if info_data['bootloader'] == 'unknown':
154 if 'STM32' in info_data['processor']:
155 info_data['bootloader'] = 'stm32-dfu'
156 elif info_data.get('manufacturer') == 'Input Club':
157 info_data['bootloader'] = 'kiibohd-dfu'
158
159 if 'STM32' in info_data['processor']:
160 info_data['platform'] = 'STM32'
161 elif 'MCU_SERIES' in rules:
162 info_data['platform'] = rules['MCU_SERIES']
163 elif 'ARM_ATSAM' in rules:
164 info_data['platform'] = 'ARM_ATSAM'
165
166 return info_data
167
168
169def avr_processor_rules(info_data, rules):
170 """Setup the default info for an AVR board.
171 """
172 info_data['processor_type'] = 'avr'
173 info_data['bootloader'] = rules['BOOTLOADER'] if 'BOOTLOADER' in rules else 'atmel-dfu'
174 info_data['platform'] = rules['ARCH'] if 'ARCH' in rules else 'unknown'
175 info_data['processor'] = rules['MCU'] if 'MCU' in rules else 'unknown'
176 info_data['protocol'] = 'V-USB' if rules.get('MCU') in VUSB_PROCESSORS else 'LUFA'
177
178 # FIXME(fauxpark/anyone): Eventually we should detect the protocol by looking at PROTOCOL inherited from mcu_selection.mk:
179 # info_data['protocol'] = 'V-USB' if rules.get('PROTOCOL') == 'VUSB' else 'LUFA'
180
181 return info_data
182
183
184def unknown_processor_rules(info_data, rules):
185 """Setup the default keyboard info for unknown boards.
186 """
187 info_data['bootloader'] = 'unknown'
188 info_data['platform'] = 'unknown'
189 info_data['processor'] = 'unknown'
190 info_data['processor_type'] = 'unknown'
191 info_data['protocol'] = 'unknown'
192
193 return info_data
194
195
196def merge_info_jsons(keyboard, info_data):
197 """Return a merged copy of all the info.json files for a keyboard.
198 """
199 for info_file in find_info_json(keyboard):
200 # Load and validate the JSON data
201 with info_file.open('r') as info_fd:
202 new_info_data = json.load(info_fd)
203
204 if not isinstance(new_info_data, dict):
205 cli.log.error("Invalid file %s, root object should be a dictionary.", str(info_file))
206 continue
207
208 # Copy whitelisted keys into `info_data`
209 for key in ('keyboard_name', 'manufacturer', 'identifier', 'url', 'maintainer', 'processor', 'bootloader', 'width', 'height'):
210 if key in new_info_data:
211 info_data[key] = new_info_data[key]
212
213 # Merge the layouts in
214 if 'layouts' in new_info_data:
215 for layout_name, json_layout in new_info_data['layouts'].items():
216 # Only pull in layouts we have a macro for
217 if layout_name in info_data['layouts']:
218 if info_data['layouts'][layout_name]['key_count'] != len(json_layout['layout']):
219 cli.log.error('%s: %s: Number of elements in info.json does not match! info.json:%s != %s:%s', info_data['keyboard_folder'], layout_name, len(json_layout['layout']), layout_name, len(info_data['layouts'][layout_name]['layout']))
220 else:
221 for i, key in enumerate(info_data['layouts'][layout_name]['layout']):
222 key.update(json_layout['layout'][i])
223
224 return info_data
225
226
227def find_info_json(keyboard):
228 """Finds all the info.json files associated with a keyboard.
229 """
230 # Find the most specific first
231 base_path = Path('keyboards')
232 keyboard_path = base_path / keyboard
233 keyboard_parent = keyboard_path.parent
234 info_jsons = [keyboard_path / 'info.json']
235
236 # Add DEFAULT_FOLDER before parents, if present
237 rules = rules_mk(keyboard)
238 if 'DEFAULT_FOLDER' in rules:
239 info_jsons.append(Path(rules['DEFAULT_FOLDER']) / 'info.json')
240
241 # Add in parent folders for least specific
242 for _ in range(5):
243 info_jsons.append(keyboard_parent / 'info.json')
244 if keyboard_parent.parent == base_path:
245 break
246 keyboard_parent = keyboard_parent.parent
247
248 # Return a list of the info.json files that actually exist
249 return [info_json for info_json in info_jsons if info_json.exists()]
diff --git a/lib/python/qmk/keyboard.py b/lib/python/qmk/keyboard.py
new file mode 100644
index 000000000..d1f2a301d
--- /dev/null
+++ b/lib/python/qmk/keyboard.py
@@ -0,0 +1,111 @@
1"""Functions that help us work with keyboards.
2"""
3from array import array
4from math import ceil
5from pathlib import Path
6
7from qmk.c_parse import parse_config_h_file
8from qmk.makefile import parse_rules_mk_file
9
10
11def config_h(keyboard):
12 """Parses all the config.h files for a keyboard.
13
14 Args:
15 keyboard: name of the keyboard
16
17 Returns:
18 a dictionary representing the content of the entire config.h tree for a keyboard
19 """
20 config = {}
21 cur_dir = Path('keyboards')
22 rules = rules_mk(keyboard)
23 keyboard = Path(rules['DEFAULT_FOLDER'] if 'DEFAULT_FOLDER' in rules else keyboard)
24
25 for dir in keyboard.parts:
26 cur_dir = cur_dir / dir
27 config = {**config, **parse_config_h_file(cur_dir / 'config.h')}
28
29 return config
30
31
32def rules_mk(keyboard):
33 """Get a rules.mk for a keyboard
34
35 Args:
36 keyboard: name of the keyboard
37
38 Returns:
39 a dictionary representing the content of the entire rules.mk tree for a keyboard
40 """
41 keyboard = Path(keyboard)
42 cur_dir = Path('keyboards')
43 rules = parse_rules_mk_file(cur_dir / keyboard / 'rules.mk')
44
45 if 'DEFAULT_FOLDER' in rules:
46 keyboard = Path(rules['DEFAULT_FOLDER'])
47
48 for i, dir in enumerate(keyboard.parts):
49 cur_dir = cur_dir / dir
50 rules = parse_rules_mk_file(cur_dir / 'rules.mk', rules)
51
52 return rules
53
54
55def render_layout(layout_data, key_labels=None):
56 """Renders a single layout.
57 """
58 textpad = [array('u', ' ' * 200) for x in range(50)]
59
60 for key in layout_data:
61 x = ceil(key.get('x', 0) * 4)
62 y = ceil(key.get('y', 0) * 3)
63 w = ceil(key.get('w', 1) * 4)
64 h = ceil(key.get('h', 1) * 3)
65
66 if key_labels:
67 label = key_labels.pop(0)
68 if label.startswith('KC_'):
69 label = label[3:]
70 else:
71 label = key.get('label', '')
72
73 label_len = w - 2
74 label_leftover = label_len - len(label)
75
76 if len(label) > label_len:
77 label = label[:label_len]
78
79 label_blank = ' ' * label_len
80 label_border = '─' * label_len
81 label_middle = label + ' '*label_leftover # noqa: yapf insists there be no whitespace around *
82
83 top_line = array('u', '┌' + label_border + '┐')
84 lab_line = array('u', '│' + label_middle + '│')
85 mid_line = array('u', '│' + label_blank + '│')
86 bot_line = array('u', '└' + label_border + "┘")
87
88 textpad[y][x:x + w] = top_line
89 textpad[y + 1][x:x + w] = lab_line
90 for i in range(h - 3):
91 textpad[y + i + 2][x:x + w] = mid_line
92 textpad[y + h - 1][x:x + w] = bot_line
93
94 lines = []
95 for line in textpad:
96 if line.tounicode().strip():
97 lines.append(line.tounicode().rstrip())
98
99 return '\n'.join(lines)
100
101
102def render_layouts(info_json):
103 """Renders all the layouts from an `info_json` structure.
104 """
105 layouts = {}
106
107 for layout in info_json['layouts']:
108 layout_data = info_json['layouts'][layout]['layout']
109 layouts[layout] = render_layout(layout_data)
110
111 return layouts
diff --git a/lib/python/qmk/keymap.py b/lib/python/qmk/keymap.py
index 69cdc8d5b..70730eb4a 100644
--- a/lib/python/qmk/keymap.py
+++ b/lib/python/qmk/keymap.py
@@ -2,8 +2,8 @@
2""" 2"""
3from pathlib import Path 3from pathlib import Path
4 4
5import qmk.path 5from qmk.path import is_keyboard
6import qmk.makefile 6from qmk.keyboard import rules_mk
7 7
8# The `keymap.c` template to use when a keyboard doesn't have its own 8# The `keymap.c` template to use when a keyboard doesn't have its own
9DEFAULT_KEYMAP_C = """#include QMK_KEYBOARD_H 9DEFAULT_KEYMAP_C = """#include QMK_KEYBOARD_H
@@ -47,6 +47,14 @@ def _strip_any(keycode):
47 return keycode 47 return keycode
48 48
49 49
50def is_keymap_dir(keymap):
51 """Return True if Path object `keymap` has a keymap file inside.
52 """
53 for file in ('keymap.c', 'keymap.json'):
54 if (keymap / file).is_file():
55 return True
56
57
50def generate(keyboard, layout, layers): 58def generate(keyboard, layout, layers):
51 """Returns a keymap.c for the specified keyboard, layout, and layers. 59 """Returns a keymap.c for the specified keyboard, layout, and layers.
52 60
@@ -95,7 +103,7 @@ def write(keyboard, keymap, layout, layers):
95 An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode. 103 An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode.
96 """ 104 """
97 keymap_c = generate(keyboard, layout, layers) 105 keymap_c = generate(keyboard, layout, layers)
98 keymap_file = qmk.path.keymap(keyboard) / keymap / 'keymap.c' 106 keymap_file = keymap(keyboard) / keymap / 'keymap.c'
99 107
100 keymap_file.parent.mkdir(parents=True, exist_ok=True) 108 keymap_file.parent.mkdir(parents=True, exist_ok=True)
101 keymap_file.write_text(keymap_c) 109 keymap_file.write_text(keymap_c)
@@ -103,37 +111,76 @@ def write(keyboard, keymap, layout, layers):
103 return keymap_file 111 return keymap_file
104 112
105 113
106def list_keymaps(keyboard_name): 114def locate_keymap(keyboard, keymap):
115 """Returns the path to a keymap for a specific keyboard.
116 """
117 if not is_keyboard(keyboard):
118 raise KeyError('Invalid keyboard: ' + repr(keyboard))
119
120 # Check the keyboard folder first, last match wins
121 checked_dirs = ''
122 keymap_path = ''
123
124 for dir in keyboard.split('/'):
125 if checked_dirs:
126 checked_dirs = '/'.join((checked_dirs, dir))
127 else:
128 checked_dirs = dir
129
130 keymap_dir = Path('keyboards') / checked_dirs / 'keymaps'
131
132 if (keymap_dir / keymap / 'keymap.c').exists():
133 keymap_path = keymap_dir / keymap / 'keymap.c'
134 if (keymap_dir / keymap / 'keymap.json').exists():
135 keymap_path = keymap_dir / keymap / 'keymap.json'
136
137 if keymap_path:
138 return keymap_path
139
140 # Check community layouts as a fallback
141 rules = rules_mk(keyboard)
142
143 if "LAYOUTS" in rules:
144 for layout in rules["LAYOUTS"].split():
145 community_layout = Path('layouts/community') / layout / keymap
146 if community_layout.exists():
147 if (community_layout / 'keymap.json').exists():
148 return community_layout / 'keymap.json'
149 if (community_layout / 'keymap.c').exists():
150 return community_layout / 'keymap.c'
151
152
153def list_keymaps(keyboard):
107 """ List the available keymaps for a keyboard. 154 """ List the available keymaps for a keyboard.
108 155
109 Args: 156 Args:
110 keyboard_name: the keyboards full name with vendor and revision if necessary, example: clueboard/66/rev3 157 keyboard: the keyboards full name with vendor and revision if necessary, example: clueboard/66/rev3
111 158
112 Returns: 159 Returns:
113 a set with the names of the available keymaps 160 a set with the names of the available keymaps
114 """ 161 """
115 # parse all the rules.mk files for the keyboard 162 # parse all the rules.mk files for the keyboard
116 rules_mk = qmk.makefile.get_rules_mk(keyboard_name) 163 rules = rules_mk(keyboard)
117 names = set() 164 names = set()
118 165
119 if rules_mk: 166 if rules:
120 # qmk_firmware/keyboards 167 # qmk_firmware/keyboards
121 keyboards_dir = Path.cwd() / "keyboards" 168 keyboards_dir = Path('keyboards')
122 # path to the keyboard's directory 169 # path to the keyboard's directory
123 kb_path = keyboards_dir / keyboard_name 170 kb_path = keyboards_dir / keyboard
124 # walk up the directory tree until keyboards_dir 171 # walk up the directory tree until keyboards_dir
125 # and collect all directories' name with keymap.c file in it 172 # and collect all directories' name with keymap.c file in it
126 while kb_path != keyboards_dir: 173 while kb_path != keyboards_dir:
127 keymaps_dir = kb_path / "keymaps" 174 keymaps_dir = kb_path / "keymaps"
128 if keymaps_dir.exists(): 175 if keymaps_dir.exists():
129 names = names.union([keymap for keymap in keymaps_dir.iterdir() if (keymaps_dir / keymap / "keymap.c").is_file()]) 176 names = names.union([keymap.name for keymap in keymaps_dir.iterdir() if is_keymap_dir(keymap)])
130 kb_path = kb_path.parent 177 kb_path = kb_path.parent
131 178
132 # if community layouts are supported, get them 179 # if community layouts are supported, get them
133 if "LAYOUTS" in rules_mk: 180 if "LAYOUTS" in rules:
134 for layout in rules_mk["LAYOUTS"].split(): 181 for layout in rules["LAYOUTS"].split():
135 cl_path = Path.cwd() / "layouts" / "community" / layout 182 cl_path = Path('layouts/community') / layout
136 if cl_path.exists(): 183 if cl_path.exists():
137 names = names.union([keymap for keymap in cl_path.iterdir() if (cl_path / keymap / "keymap.c").is_file()]) 184 names = names.union([keymap.name for keymap in cl_path.iterdir() if is_keymap_dir(keymap)])
138 185
139 return sorted(names) 186 return sorted(names)
diff --git a/lib/python/qmk/makefile.py b/lib/python/qmk/makefile.py
index 8645056d2..02c2e7005 100644
--- a/lib/python/qmk/makefile.py
+++ b/lib/python/qmk/makefile.py
@@ -2,8 +2,6 @@
2""" 2"""
3from pathlib import Path 3from pathlib import Path
4 4
5from qmk.errors import NoSuchKeyboardError
6
7 5
8def parse_rules_mk_file(file, rules_mk=None): 6def parse_rules_mk_file(file, rules_mk=None):
9 """Turn a rules.mk file into a dictionary. 7 """Turn a rules.mk file into a dictionary.
@@ -51,33 +49,3 @@ def parse_rules_mk_file(file, rules_mk=None):
51 rules_mk[key.strip()] = value.strip() 49 rules_mk[key.strip()] = value.strip()
52 50
53 return rules_mk 51 return rules_mk
54
55
56def get_rules_mk(keyboard):
57 """ Get a rules.mk for a keyboard
58
59 Args:
60 keyboard: name of the keyboard
61
62 Raises:
63 NoSuchKeyboardError: when the keyboard does not exists
64
65 Returns:
66 a dictionary with the content of the rules.mk file
67 """
68 # Start with qmk_firmware/keyboards
69 kb_path = Path.cwd() / "keyboards"
70 # walk down the directory tree
71 # and collect all rules.mk files
72 kb_dir = kb_path / keyboard
73 if kb_dir.exists():
74 rules_mk = dict()
75 for directory in Path(keyboard).parts:
76 kb_path = kb_path / directory
77 rules_mk_path = kb_path / "rules.mk"
78 if rules_mk_path.exists():
79 rules_mk = parse_rules_mk_file(rules_mk_path, rules_mk)
80 else:
81 raise NoSuchKeyboardError("The requested keyboard and/or revision does not exist.")
82
83 return rules_mk
diff --git a/lib/python/qmk/math.py b/lib/python/qmk/math.py
new file mode 100644
index 000000000..88dc4a300
--- /dev/null
+++ b/lib/python/qmk/math.py
@@ -0,0 +1,33 @@
1"""Parse arbitrary math equations in a safe way.
2
3Gratefully copied from https://stackoverflow.com/a/9558001
4"""
5import ast
6import operator as op
7
8# supported operators
9operators = {ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul, ast.Div: op.truediv, ast.Pow: op.pow, ast.BitXor: op.xor, ast.USub: op.neg}
10
11
12def compute(expr):
13 """Parse a mathematical expression and return the answer.
14
15 >>> compute('2^6')
16 4
17 >>> compute('2**6')
18 64
19 >>> compute('1 + 2*3**(4^5) / (6 + -7)')
20 -5.0
21 """
22 return _eval(ast.parse(expr, mode='eval').body)
23
24
25def _eval(node):
26 if isinstance(node, ast.Num): # <number>
27 return node.n
28 elif isinstance(node, ast.BinOp): # <left> <operator> <right>
29 return operators[type(node.op)](_eval(node.left), _eval(node.right))
30 elif isinstance(node, ast.UnaryOp): # <operator> <operand> e.g., -1
31 return operators[type(node.op)](_eval(node.operand))
32 else:
33 raise TypeError(node)
diff --git a/lib/python/qmk/path.py b/lib/python/qmk/path.py
index 7306c433b..8df6f0e91 100644
--- a/lib/python/qmk/path.py
+++ b/lib/python/qmk/path.py
@@ -4,20 +4,10 @@ import logging
4import os 4import os
5from pathlib import Path 5from pathlib import Path
6 6
7from qmk.constants import QMK_FIRMWARE, MAX_KEYBOARD_SUBFOLDERS 7from qmk.constants import MAX_KEYBOARD_SUBFOLDERS, QMK_FIRMWARE
8from qmk.errors import NoSuchKeyboardError 8from qmk.errors import NoSuchKeyboardError
9 9
10 10
11def is_keymap_dir(keymap_path):
12 """Returns True if `keymap_path` is a valid keymap directory.
13 """
14 keymap_path = Path(keymap_path)
15 keymap_c = keymap_path / 'keymap.c'
16 keymap_json = keymap_path / 'keymap.json'
17
18 return any((keymap_c.exists(), keymap_json.exists()))
19
20
21def is_keyboard(keyboard_name): 11def is_keyboard(keyboard_name):
22 """Returns True if `keyboard_name` is a keyboard we can compile. 12 """Returns True if `keyboard_name` is a keyboard we can compile.
23 """ 13 """
@@ -68,17 +58,3 @@ def normpath(path):
68 return path 58 return path
69 59
70 return Path(os.environ['ORIG_CWD']) / path 60 return Path(os.environ['ORIG_CWD']) / path
71
72
73def c_source_files(dir_names):
74 """Returns a list of all *.c, *.h, and *.cpp files for a given list of directories
75
76 Args:
77
78 dir_names
79 List of directories, relative pathing starts at qmk's cwd
80 """
81 files = []
82 for dir in dir_names:
83 files.extend(file for file in Path(dir).glob('**/*') if file.suffix in ['.c', '.h', '.cpp'])
84 return files
diff --git a/lib/python/qmk/tests/test_cli_commands.py b/lib/python/qmk/tests/test_cli_commands.py
index 768929de1..dce270de8 100644
--- a/lib/python/qmk/tests/test_cli_commands.py
+++ b/lib/python/qmk/tests/test_cli_commands.py
@@ -4,89 +4,151 @@ from qmk.commands import run
4 4
5def check_subcommand(command, *args): 5def check_subcommand(command, *args):
6 cmd = ['bin/qmk', command] + list(args) 6 cmd = ['bin/qmk', command] + list(args)
7 return run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) 7 result = run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
8 return result
9
10
11def check_returncode(result, expected=0):
12 """Print stdout if `result.returncode` does not match `expected`.
13 """
14 if result.returncode != expected:
15 print('`%s` stdout:' % ' '.join(result.args))
16 print(result.stdout)
17 print('returncode:', result.returncode)
18 assert result.returncode == expected
8 19
9 20
10def test_cformat(): 21def test_cformat():
11 result = check_subcommand('cformat', 'quantum/matrix.c') 22 result = check_subcommand('cformat', 'quantum/matrix.c')
12 assert result.returncode == 0 23 check_returncode(result)
13 24
14 25
15def test_compile(): 26def test_compile():
16 assert check_subcommand('compile', '-kb', 'handwired/onekey/pytest', '-km', 'default').returncode == 0 27 result = check_subcommand('compile', '-kb', 'handwired/onekey/pytest', '-km', 'default', '-n')
28 check_returncode(result)
17 29
18 30
19def test_flash(): 31def test_flash():
20 assert check_subcommand('flash', '-b').returncode == 1 32 result = check_subcommand('flash', '-kb', 'handwired/onekey/pytest', '-km', 'default', '-n')
21 assert check_subcommand('flash').returncode == 1 33 check_returncode(result)
34
35
36def test_flash_bootloaders():
37 result = check_subcommand('flash', '-b')
38 check_returncode(result, 1)
22 39
23 40
24def test_config(): 41def test_config():
25 result = check_subcommand('config') 42 result = check_subcommand('config')
26 assert result.returncode == 0 43 check_returncode(result)
27 assert 'general.color' in result.stdout 44 assert 'general.color' in result.stdout
28 45
29 46
30def test_kle2json(): 47def test_kle2json():
31 assert check_subcommand('kle2json', 'kle.txt', '-f').returncode == 0 48 result = check_subcommand('kle2json', 'kle.txt', '-f')
49 check_returncode(result)
32 50
33 51
34def test_doctor(): 52def test_doctor():
35 result = check_subcommand('doctor', '-n') 53 result = check_subcommand('doctor', '-n')
36 assert result.returncode == 0 54 check_returncode(result)
37 assert 'QMK Doctor is checking your environment.' in result.stderr 55 assert 'QMK Doctor is checking your environment.' in result.stdout
38 assert 'QMK is ready to go' in result.stderr 56 assert 'QMK is ready to go' in result.stdout
39 57
40 58
41def test_hello(): 59def test_hello():
42 result = check_subcommand('hello') 60 result = check_subcommand('hello')
43 assert result.returncode == 0 61 check_returncode(result)
44 assert 'Hello,' in result.stderr 62 assert 'Hello,' in result.stdout
45 63
46 64
47def test_pyformat(): 65def test_pyformat():
48 result = check_subcommand('pyformat') 66 result = check_subcommand('pyformat')
49 assert result.returncode == 0 67 check_returncode(result)
50 assert 'Successfully formatted the python code' in result.stderr 68 assert 'Successfully formatted the python code' in result.stdout
69
70
71def test_list_keyboards():
72 result = check_subcommand('list-keyboards')
73 check_returncode(result)
74 # check to see if a known keyboard is returned
75 # this will fail if handwired/onekey/pytest is removed
76 assert 'handwired/onekey/pytest' in result.stdout
51 77
52 78
53def test_list_keymaps(): 79def test_list_keymaps():
54 result = check_subcommand('list-keymaps', '-kb', 'handwired/onekey/pytest') 80 result = check_subcommand('list-keymaps', '-kb', 'handwired/onekey/pytest')
55 assert result.returncode == 0 81 check_returncode(result, 0)
56 assert 'default' and 'test' in result.stdout 82 assert 'default' and 'test' in result.stdout
57 83
58 84
59def test_list_keymaps_long(): 85def test_list_keymaps_long():
60 result = check_subcommand('list-keymaps', '--keyboard', 'handwired/onekey/pytest') 86 result = check_subcommand('list-keymaps', '--keyboard', 'handwired/onekey/pytest')
61 assert result.returncode == 0 87 check_returncode(result, 0)
62 assert 'default' and 'test' in result.stdout 88 assert 'default' and 'test' in result.stdout
63 89
64 90
65def test_list_keymaps_kb_only(): 91def test_list_keymaps_kb_only():
66 result = check_subcommand('list-keymaps', '-kb', 'niu_mini') 92 result = check_subcommand('list-keymaps', '-kb', 'niu_mini')
67 assert result.returncode == 0 93 check_returncode(result, 0)
68 assert 'default' and 'via' in result.stdout 94 assert 'default' and 'via' in result.stdout
69 95
70 96
71def test_list_keymaps_vendor_kb(): 97def test_list_keymaps_vendor_kb():
72 result = check_subcommand('list-keymaps', '-kb', 'ai03/lunar') 98 result = check_subcommand('list-keymaps', '-kb', 'ai03/lunar')
73 assert result.returncode == 0 99 check_returncode(result, 0)
74 assert 'default' and 'via' in result.stdout 100 assert 'default' and 'via' in result.stdout
75 101
76 102
77def test_list_keymaps_vendor_kb_rev(): 103def test_list_keymaps_vendor_kb_rev():
78 result = check_subcommand('list-keymaps', '-kb', 'kbdfans/kbd67/mkiirgb/v2') 104 result = check_subcommand('list-keymaps', '-kb', 'kbdfans/kbd67/mkiirgb/v2')
79 assert result.returncode == 0 105 check_returncode(result, 0)
80 assert 'default' and 'via' in result.stdout 106 assert 'default' and 'via' in result.stdout
81 107
82 108
83def test_list_keymaps_no_keyboard_found(): 109def test_list_keymaps_no_keyboard_found():
84 result = check_subcommand('list-keymaps', '-kb', 'asdfghjkl') 110 result = check_subcommand('list-keymaps', '-kb', 'asdfghjkl')
85 assert result.returncode == 0 111 check_returncode(result, 1)
86 assert 'does not exist' in result.stdout 112 assert 'does not exist' in result.stdout
87 113
88 114
89def test_json2c(): 115def test_json2c():
90 result = check_subcommand('json2c', 'keyboards/handwired/onekey/keymaps/default_json/keymap.json') 116 result = check_subcommand('json2c', 'keyboards/handwired/onekey/keymaps/default_json/keymap.json')
91 assert result.returncode == 0 117 check_returncode(result, 0)
92 assert result.stdout == '#include QMK_KEYBOARD_H\nconst uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {\t[0] = LAYOUT(KC_A)};\n\n' 118 assert result.stdout == '#include QMK_KEYBOARD_H\nconst uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {\t[0] = LAYOUT(KC_A)};\n\n'
119
120
121def test_info():
122 result = check_subcommand('info', '-kb', 'handwired/onekey/pytest')
123 check_returncode(result)
124 assert 'Keyboard Name: handwired/onekey/pytest' in result.stdout
125 assert 'Processor: STM32F303' in result.stdout
126 assert 'Layout:' not in result.stdout
127 assert 'k0' not in result.stdout
128
129
130def test_info_keyboard_render():
131 result = check_subcommand('info', '-kb', 'handwired/onekey/pytest', '-l')
132 check_returncode(result)
133 assert 'Keyboard Name: handwired/onekey/pytest' in result.stdout
134 assert 'Processor: STM32F303' in result.stdout
135 assert 'Layout:' in result.stdout
136 assert 'k0' in result.stdout
137
138
139def test_info_keymap_render():
140 result = check_subcommand('info', '-kb', 'handwired/onekey/pytest', '-km', 'default_json')
141 check_returncode(result)
142 assert 'Keyboard Name: handwired/onekey/pytest' in result.stdout
143 assert 'Processor: STM32F303' in result.stdout
144 assert '│A │' in result.stdout
145
146
147def test_info_matrix_render():
148 result = check_subcommand('info', '-kb', 'handwired/onekey/pytest', '-m')
149 check_returncode(result)
150 assert 'Keyboard Name: handwired/onekey/pytest' in result.stdout
151 assert 'Processor: STM32F303' in result.stdout
152 assert 'LAYOUT' in result.stdout
153 assert '│0A│' in result.stdout
154 assert 'Matrix for "LAYOUT"' in result.stdout