aboutsummaryrefslogtreecommitdiff
path: root/lib/python/qmk/cli
diff options
context:
space:
mode:
Diffstat (limited to 'lib/python/qmk/cli')
-rw-r--r--lib/python/qmk/cli/__init__.py4
-rwxr-xr-x[-rw-r--r--]lib/python/qmk/cli/cformat.py139
-rwxr-xr-xlib/python/qmk/cli/compile.py2
-rw-r--r--lib/python/qmk/cli/console.py5
-rwxr-xr-xlib/python/qmk/cli/doctor/__init__.py5
-rw-r--r--lib/python/qmk/cli/doctor/check.py164
-rw-r--r--lib/python/qmk/cli/doctor/linux.py172
-rw-r--r--lib/python/qmk/cli/doctor/macos.py13
-rwxr-xr-xlib/python/qmk/cli/doctor/main.py (renamed from lib/python/qmk/cli/doctor.py)76
-rw-r--r--lib/python/qmk/cli/doctor/windows.py14
-rwxr-xr-x[-rw-r--r--]lib/python/qmk/cli/fileformat.py24
-rw-r--r--lib/python/qmk/cli/flash.py2
-rw-r--r--lib/python/qmk/cli/format/c.py137
-rwxr-xr-xlib/python/qmk/cli/format/json.py5
-rwxr-xr-xlib/python/qmk/cli/format/python.py26
-rw-r--r--lib/python/qmk/cli/format/text.py27
-rwxr-xr-xlib/python/qmk/cli/generate/config_h.py140
-rwxr-xr-xlib/python/qmk/cli/generate/info_json.py40
-rwxr-xr-xlib/python/qmk/cli/generate/rules_mk.py11
-rw-r--r--lib/python/qmk/cli/generate/version_h.py28
-rwxr-xr-xlib/python/qmk/cli/info.py55
-rwxr-xr-xlib/python/qmk/cli/kle2json.py2
-rwxr-xr-xlib/python/qmk/cli/multibuild.py6
-rw-r--r--lib/python/qmk/cli/new/keyboard.py141
-rwxr-xr-xlib/python/qmk/cli/pyformat.py32
25 files changed, 993 insertions, 277 deletions
diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py
index de71a5d1e..b22f1c0d2 100644
--- a/lib/python/qmk/cli/__init__.py
+++ b/lib/python/qmk/cli/__init__.py
@@ -40,7 +40,10 @@ subcommands = [
40 'qmk.cli.doctor', 40 'qmk.cli.doctor',
41 'qmk.cli.fileformat', 41 'qmk.cli.fileformat',
42 'qmk.cli.flash', 42 'qmk.cli.flash',
43 'qmk.cli.format.c',
43 'qmk.cli.format.json', 44 'qmk.cli.format.json',
45 'qmk.cli.format.python',
46 'qmk.cli.format.text',
44 'qmk.cli.generate.api', 47 'qmk.cli.generate.api',
45 'qmk.cli.generate.config_h', 48 'qmk.cli.generate.config_h',
46 'qmk.cli.generate.dfu_header', 49 'qmk.cli.generate.dfu_header',
@@ -50,6 +53,7 @@ subcommands = [
50 'qmk.cli.generate.layouts', 53 'qmk.cli.generate.layouts',
51 'qmk.cli.generate.rgb_breathe_table', 54 'qmk.cli.generate.rgb_breathe_table',
52 'qmk.cli.generate.rules_mk', 55 'qmk.cli.generate.rules_mk',
56 'qmk.cli.generate.version_h',
53 'qmk.cli.hello', 57 'qmk.cli.hello',
54 'qmk.cli.info', 58 'qmk.cli.info',
55 'qmk.cli.json2c', 59 'qmk.cli.json2c',
diff --git a/lib/python/qmk/cli/cformat.py b/lib/python/qmk/cli/cformat.py
index efeb45967..9d0ecaeba 100644..100755
--- a/lib/python/qmk/cli/cformat.py
+++ b/lib/python/qmk/cli/cformat.py
@@ -1,137 +1,28 @@
1"""Format C code according to QMK's style. 1"""Point people to the new command name.
2""" 2"""
3from os import path 3import sys
4from shutil import which 4from pathlib import Path
5from subprocess import CalledProcessError, DEVNULL, Popen, PIPE
6 5
7from argcomplete.completers import FilesCompleter
8from milc import cli 6from milc import cli
9 7
10from qmk.path import normpath
11from qmk.c_parse import c_source_files
12
13c_file_suffixes = ('c', 'h', 'cpp')
14core_dirs = ('drivers', 'quantum', 'tests', 'tmk_core', 'platforms')
15ignored = ('tmk_core/protocol/usb_hid', 'quantum/template', 'platforms/chibios')
16
17
18def find_clang_format():
19 """Returns the path to clang-format.
20 """
21 for clang_version in range(20, 6, -1):
22 binary = f'clang-format-{clang_version}'
23
24 if which(binary):
25 return binary
26
27 return 'clang-format'
28
29
30def find_diffs(files):
31 """Run clang-format and diff it against a file.
32 """
33 found_diffs = False
34
35 for file in files:
36 cli.log.debug('Checking for changes in %s', file)
37 clang_format = Popen([find_clang_format(), file], stdout=PIPE, stderr=PIPE, universal_newlines=True)
38 diff = cli.run(['diff', '-u', f'--label=a/{file}', f'--label=b/{file}', str(file), '-'], stdin=clang_format.stdout, capture_output=True)
39
40 if diff.returncode != 0:
41 print(diff.stdout)
42 found_diffs = True
43
44 return found_diffs
45
46
47def cformat_run(files):
48 """Spawn clang-format subprocess with proper arguments
49 """
50 # Determine which version of clang-format to use
51 clang_format = [find_clang_format(), '-i']
52
53 try:
54 cli.run([*clang_format, *map(str, files)], check=True, capture_output=False, stdin=DEVNULL)
55 cli.log.info('Successfully formatted the C code.')
56 return True
57
58 except CalledProcessError as e:
59 cli.log.error('Error formatting C code!')
60 cli.log.debug('%s exited with returncode %s', e.cmd, e.returncode)
61 cli.log.debug('STDOUT:')
62 cli.log.debug(e.stdout)
63 cli.log.debug('STDERR:')
64 cli.log.debug(e.stderr)
65 return False
66
67
68def filter_files(files, core_only=False):
69 """Yield only files to be formatted and skip the rest
70 """
71 if core_only:
72 # Filter non-core files
73 for index, file in enumerate(files):
74 # The following statement checks each file to see if the file path is
75 # - in the core directories
76 # - not in the ignored directories
77 if not any(i in str(file) for i in core_dirs) or any(i in str(file) for i in ignored):
78 files[index] = None
79 cli.log.debug("Skipping non-core file %s, as '--core-only' is used.", file)
80
81 for file in files:
82 if file and file.name.split('.')[-1] in c_file_suffixes:
83 yield file
84 else:
85 cli.log.debug('Skipping file %s', file)
86
87 8
88@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Flag only, don't automatically format.") 9@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Flag only, don't automatically format.")
89@cli.argument('-b', '--base-branch', default='origin/master', help='Branch to compare to diffs to.') 10@cli.argument('-b', '--base-branch', default='origin/master', help='Branch to compare to diffs to.')
90@cli.argument('-a', '--all-files', arg_only=True, action='store_true', help='Format all core files.') 11@cli.argument('-a', '--all-files', arg_only=True, action='store_true', help='Format all core files.')
91@cli.argument('--core-only', arg_only=True, action='store_true', help='Format core files only.') 12@cli.argument('--core-only', arg_only=True, action='store_true', help='Format core files only.')
92@cli.argument('files', nargs='*', arg_only=True, type=normpath, completer=FilesCompleter('.c'), help='Filename(s) to format.') 13@cli.argument('files', nargs='*', arg_only=True, help='Filename(s) to format.')
93@cli.subcommand("Format C code according to QMK's style.", hidden=False if cli.config.user.developer else True) 14@cli.subcommand('Pointer to the new command name: qmk format-c.', hidden=True)
94def cformat(cli): 15def cformat(cli):
95 """Format C code according to QMK's style. 16 """Pointer to the new command name: qmk format-c.
96 """ 17 """
97 # Find the list of files to format 18 cli.log.warning('"qmk cformat" has been renamed to "qmk format-c". Please use the new command in the future.')
98 if cli.args.files: 19 argv = [sys.executable, *sys.argv]
99 files = list(filter_files(cli.args.files, cli.args.core_only)) 20 argv[argv.index('cformat')] = 'format-c'
100 21 script_path = Path(argv[1])
101 if not files: 22 script_path_exe = Path(f'{argv[1]}.exe')
102 cli.log.error('No C files in filelist: %s', ', '.join(map(str, cli.args.files)))
103 exit(0)
104
105 if cli.args.all_files:
106 cli.log.warning('Filenames passed with -a, only formatting: %s', ','.join(map(str, files)))
107
108 elif cli.args.all_files:
109 all_files = c_source_files(core_dirs)
110 files = list(filter_files(all_files, True))
111
112 else:
113 git_diff_cmd = ['git', 'diff', '--name-only', cli.args.base_branch, *core_dirs]
114 git_diff = cli.run(git_diff_cmd, stdin=DEVNULL)
115
116 if git_diff.returncode != 0:
117 cli.log.error("Error running %s", git_diff_cmd)
118 print(git_diff.stderr)
119 return git_diff.returncode
120
121 files = []
122
123 for file in git_diff.stdout.strip().split('\n'):
124 if not any([file.startswith(ignore) for ignore in ignored]):
125 if path.exists(file) and file.split('.')[-1] in c_file_suffixes:
126 files.append(file)
127 23
128 # Sanity check 24 if not script_path.exists() and script_path_exe.exists():
129 if not files: 25 # For reasons I don't understand ".exe" is stripped from the script name on windows.
130 cli.log.error('No changed files detected. Use "qmk cformat -a" to format all core files') 26 argv[1] = str(script_path_exe)
131 return False
132 27
133 # Run clang-format on the files we've found 28 return cli.run(argv, capture_output=False).returncode
134 if cli.args.dry_run:
135 return not find_diffs(files)
136 else:
137 return cformat_run(files)
diff --git a/lib/python/qmk/cli/compile.py b/lib/python/qmk/cli/compile.py
index 7a45e7721..acbd77864 100755
--- a/lib/python/qmk/cli/compile.py
+++ b/lib/python/qmk/cli/compile.py
@@ -18,7 +18,7 @@ from qmk.keymap import keymap_completer
18@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='The keyboard to build a firmware for. Ignored when a configurator export is supplied.') 18@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='The keyboard to build a firmware for. Ignored when a configurator export is supplied.')
19@cli.argument('-km', '--keymap', completer=keymap_completer, help='The keymap to build a firmware for. Ignored when a configurator export is supplied.') 19@cli.argument('-km', '--keymap', completer=keymap_completer, help='The keymap to build a firmware for. Ignored when a configurator export is supplied.')
20@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually build, just show the make command to be run.") 20@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually build, just show the make command to be run.")
21@cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs to run.") 21@cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs; 0 means unlimited.")
22@cli.argument('-e', '--env', arg_only=True, action='append', default=[], help="Set a variable to be passed to make. May be passed multiple times.") 22@cli.argument('-e', '--env', arg_only=True, action='append', default=[], help="Set a variable to be passed to make. May be passed multiple times.")
23@cli.argument('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.") 23@cli.argument('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.")
24@cli.subcommand('Compile a QMK Firmware.') 24@cli.subcommand('Compile a QMK Firmware.')
diff --git a/lib/python/qmk/cli/console.py b/lib/python/qmk/cli/console.py
index 45ff0c8be..3c508160e 100644
--- a/lib/python/qmk/cli/console.py
+++ b/lib/python/qmk/cli/console.py
@@ -48,10 +48,11 @@ KNOWN_BOOTLOADERS = {
48 ('239A', '000C'): 'caterina: Adafruit Feather 32U4', 48 ('239A', '000C'): 'caterina: Adafruit Feather 32U4',
49 ('239A', '000D'): 'caterina: Adafruit ItsyBitsy 32U4 3v', 49 ('239A', '000D'): 'caterina: Adafruit ItsyBitsy 32U4 3v',
50 ('239A', '000E'): 'caterina: Adafruit ItsyBitsy 32U4 5v', 50 ('239A', '000E'): 'caterina: Adafruit ItsyBitsy 32U4 5v',
51 ('239A', '000E'): 'caterina: Adafruit ItsyBitsy 32U4 5v',
52 ('2A03', '0036'): 'caterina: Arduino Leonardo', 51 ('2A03', '0036'): 'caterina: Arduino Leonardo',
53 ('2A03', '0037'): 'caterina: Arduino Micro', 52 ('2A03', '0037'): 'caterina: Arduino Micro',
54 ('314B', '0106'): 'apm32-dfu: APM32 DFU ISP Mode' 53 ('314B', '0106'): 'apm32-dfu: APM32 DFU ISP Mode',
54 ('03EB', '2067'): 'qmk-hid: HID Bootloader',
55 ('03EB', '2045'): 'lufa-ms: LUFA Mass Storage Bootloader'
55} 56}
56 57
57 58
diff --git a/lib/python/qmk/cli/doctor/__init__.py b/lib/python/qmk/cli/doctor/__init__.py
new file mode 100755
index 000000000..272e04202
--- /dev/null
+++ b/lib/python/qmk/cli/doctor/__init__.py
@@ -0,0 +1,5 @@
1"""QMK Doctor
2
3Check out the user's QMK environment and make sure it's ready to compile.
4"""
5from .main import doctor
diff --git a/lib/python/qmk/cli/doctor/check.py b/lib/python/qmk/cli/doctor/check.py
new file mode 100644
index 000000000..0807f4151
--- /dev/null
+++ b/lib/python/qmk/cli/doctor/check.py
@@ -0,0 +1,164 @@
1"""Check for specific programs.
2"""
3from enum import Enum
4import re
5import shutil
6from subprocess import DEVNULL
7
8from milc import cli
9from qmk import submodules
10from qmk.constants import QMK_FIRMWARE
11
12
13class CheckStatus(Enum):
14 OK = 1
15 WARNING = 2
16 ERROR = 3
17
18
19ESSENTIAL_BINARIES = {
20 'dfu-programmer': {},
21 'avrdude': {},
22 'dfu-util': {},
23 'avr-gcc': {
24 'version_arg': '-dumpversion'
25 },
26 'arm-none-eabi-gcc': {
27 'version_arg': '-dumpversion'
28 },
29 'bin/qmk': {},
30}
31
32
33def _parse_gcc_version(version):
34 m = re.match(r"(\d+)(?:\.(\d+))?(?:\.(\d+))?", version)
35
36 return {
37 'major': int(m.group(1)),
38 'minor': int(m.group(2)) if m.group(2) else 0,
39 'patch': int(m.group(3)) if m.group(3) else 0,
40 }
41
42
43def _check_arm_gcc_version():
44 """Returns True if the arm-none-eabi-gcc version is not known to cause problems.
45 """
46 if 'output' in ESSENTIAL_BINARIES['arm-none-eabi-gcc']:
47 version_number = ESSENTIAL_BINARIES['arm-none-eabi-gcc']['output'].strip()
48 cli.log.info('Found arm-none-eabi-gcc version %s', version_number)
49
50 return CheckStatus.OK # Right now all known arm versions are ok
51
52
53def _check_avr_gcc_version():
54 """Returns True if the avr-gcc version is not known to cause problems.
55 """
56 rc = CheckStatus.ERROR
57 if 'output' in ESSENTIAL_BINARIES['avr-gcc']:
58 version_number = ESSENTIAL_BINARIES['avr-gcc']['output'].strip()
59
60 cli.log.info('Found avr-gcc version %s', version_number)
61 rc = CheckStatus.OK
62
63 parsed_version = _parse_gcc_version(version_number)
64 if parsed_version['major'] > 8:
65 cli.log.warning('{fg_yellow}We do not recommend avr-gcc newer than 8. Downgrading to 8.x is recommended.')
66 rc = CheckStatus.WARNING
67
68 return rc
69
70
71def _check_avrdude_version():
72 if 'output' in ESSENTIAL_BINARIES['avrdude']:
73 last_line = ESSENTIAL_BINARIES['avrdude']['output'].split('\n')[-2]
74 version_number = last_line.split()[2][:-1]
75 cli.log.info('Found avrdude version %s', version_number)
76
77 return CheckStatus.OK
78
79
80def _check_dfu_util_version():
81 if 'output' in ESSENTIAL_BINARIES['dfu-util']:
82 first_line = ESSENTIAL_BINARIES['dfu-util']['output'].split('\n')[0]
83 version_number = first_line.split()[1]
84 cli.log.info('Found dfu-util version %s', version_number)
85
86 return CheckStatus.OK
87
88
89def _check_dfu_programmer_version():
90 if 'output' in ESSENTIAL_BINARIES['dfu-programmer']:
91 first_line = ESSENTIAL_BINARIES['dfu-programmer']['output'].split('\n')[0]
92 version_number = first_line.split()[1]
93 cli.log.info('Found dfu-programmer version %s', version_number)
94
95 return CheckStatus.OK
96
97
98def check_binaries():
99 """Iterates through ESSENTIAL_BINARIES and tests them.
100 """
101 ok = True
102
103 for binary in sorted(ESSENTIAL_BINARIES):
104 if not is_executable(binary):
105 ok = False
106
107 return ok
108
109
110def check_binary_versions():
111 """Check the versions of ESSENTIAL_BINARIES
112 """
113 versions = []
114 for check in (_check_arm_gcc_version, _check_avr_gcc_version, _check_avrdude_version, _check_dfu_util_version, _check_dfu_programmer_version):
115 versions.append(check())
116 return versions
117
118
119def check_submodules():
120 """Iterates through all submodules to make sure they're cloned and up to date.
121 """
122 for submodule in submodules.status().values():
123 if submodule['status'] is None:
124 cli.log.error('Submodule %s has not yet been cloned!', submodule['name'])
125 return CheckStatus.ERROR
126 elif not submodule['status']:
127 cli.log.warning('Submodule %s is not up to date!', submodule['name'])
128 return CheckStatus.WARNING
129
130 return CheckStatus.OK
131
132
133def is_executable(command):
134 """Returns True if command exists and can be executed.
135 """
136 # Make sure the command is in the path.
137 res = shutil.which(command)
138 if res is None:
139 cli.log.error("{fg_red}Can't find %s in your path.", command)
140 return False
141
142 # Make sure the command can be executed
143 version_arg = ESSENTIAL_BINARIES[command].get('version_arg', '--version')
144 check = cli.run([command, version_arg], combined_output=True, stdin=DEVNULL, timeout=5)
145
146 ESSENTIAL_BINARIES[command]['output'] = check.stdout
147
148 if check.returncode in [0, 1]: # Older versions of dfu-programmer exit 1
149 cli.log.debug('Found {fg_cyan}%s', command)
150 return True
151
152 cli.log.error("{fg_red}Can't run `%s %s`", command, version_arg)
153 return False
154
155
156def check_git_repo():
157 """Checks that the .git directory exists inside QMK_HOME.
158
159 This is a decent enough indicator that the qmk_firmware directory is a
160 proper Git repository, rather than a .zip download from GitHub.
161 """
162 dot_git = QMK_FIRMWARE / '.git'
163
164 return CheckStatus.OK if dot_git.exists() else CheckStatus.WARNING
diff --git a/lib/python/qmk/cli/doctor/linux.py b/lib/python/qmk/cli/doctor/linux.py
new file mode 100644
index 000000000..6ce00f6ef
--- /dev/null
+++ b/lib/python/qmk/cli/doctor/linux.py
@@ -0,0 +1,172 @@
1"""OS-specific functions for: Linux
2"""
3import platform
4import shutil
5from pathlib import Path
6
7from milc import cli
8
9from qmk.constants import QMK_FIRMWARE
10from .check import CheckStatus
11
12
13def _udev_rule(vid, pid=None, *args):
14 """ Helper function that return udev rules
15 """
16 rule = ""
17 if pid:
18 rule = 'SUBSYSTEMS=="usb", ATTRS{idVendor}=="%s", ATTRS{idProduct}=="%s", TAG+="uaccess"' % (
19 vid,
20 pid,
21 )
22 else:
23 rule = 'SUBSYSTEMS=="usb", ATTRS{idVendor}=="%s", TAG+="uaccess"' % vid
24 if args:
25 rule = ', '.join([rule, *args])
26 return rule
27
28
29def _deprecated_udev_rule(vid, pid=None):
30 """ Helper function that return udev rules
31
32 Note: these are no longer the recommended rules, this is just used to check for them
33 """
34 if pid:
35 return 'SUBSYSTEMS=="usb", ATTRS{idVendor}=="%s", ATTRS{idProduct}=="%s", MODE:="0666"' % (vid, pid)
36 else:
37 return 'SUBSYSTEMS=="usb", ATTRS{idVendor}=="%s", MODE:="0666"' % vid
38
39
40def check_udev_rules():
41 """Make sure the udev rules look good.
42 """
43 rc = CheckStatus.OK
44 udev_dirs = [
45 Path("/usr/lib/udev/rules.d/"),
46 Path("/usr/local/lib/udev/rules.d/"),
47 Path("/run/udev/rules.d/"),
48 Path("/etc/udev/rules.d/"),
49 ]
50 desired_rules = {
51 'atmel-dfu': {
52 _udev_rule("03eb", "2fef"), # ATmega16U2
53 _udev_rule("03eb", "2ff0"), # ATmega32U2
54 _udev_rule("03eb", "2ff3"), # ATmega16U4
55 _udev_rule("03eb", "2ff4"), # ATmega32U4
56 _udev_rule("03eb", "2ff9"), # AT90USB64
57 _udev_rule("03eb", "2ffa"), # AT90USB162
58 _udev_rule("03eb", "2ffb") # AT90USB128
59 },
60 'kiibohd': {_udev_rule("1c11", "b007")},
61 'stm32': {
62 _udev_rule("1eaf", "0003"), # STM32duino
63 _udev_rule("0483", "df11") # STM32 DFU
64 },
65 'bootloadhid': {_udev_rule("16c0", "05df")},
66 'usbasploader': {_udev_rule("16c0", "05dc")},
67 'massdrop': {_udev_rule("03eb", "6124", 'ENV{ID_MM_DEVICE_IGNORE}="1"')},
68 'caterina': {
69 # Spark Fun Electronics
70 _udev_rule("1b4f", "9203", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Pro Micro 3V3/8MHz
71 _udev_rule("1b4f", "9205", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Pro Micro 5V/16MHz
72 _udev_rule("1b4f", "9207", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # LilyPad 3V3/8MHz (and some Pro Micro clones)
73 # Pololu Electronics
74 _udev_rule("1ffb", "0101", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # A-Star 32U4
75 # Arduino SA
76 _udev_rule("2341", "0036", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Leonardo
77 _udev_rule("2341", "0037", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Micro
78 # Adafruit Industries LLC
79 _udev_rule("239a", "000c", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Feather 32U4
80 _udev_rule("239a", "000d", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # ItsyBitsy 32U4 3V3/8MHz
81 _udev_rule("239a", "000e", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # ItsyBitsy 32U4 5V/16MHz
82 # dog hunter AG
83 _udev_rule("2a03", "0036", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Leonardo
84 _udev_rule("2a03", "0037", 'ENV{ID_MM_DEVICE_IGNORE}="1"') # Micro
85 },
86 'hid-bootloader': {
87 _udev_rule("03eb", "2067"), # QMK HID
88 _udev_rule("16c0", "0478") # PJRC halfkay
89 }
90 }
91
92 # These rules are no longer recommended, only use them to check for their presence.
93 deprecated_rules = {
94 'atmel-dfu': {_deprecated_udev_rule("03eb", "2ff4"), _deprecated_udev_rule("03eb", "2ffb"), _deprecated_udev_rule("03eb", "2ff0")},
95 'kiibohd': {_deprecated_udev_rule("1c11")},
96 'stm32': {_deprecated_udev_rule("1eaf", "0003"), _deprecated_udev_rule("0483", "df11")},
97 'bootloadhid': {_deprecated_udev_rule("16c0", "05df")},
98 'caterina': {'ATTRS{idVendor}=="2a03", ENV{ID_MM_DEVICE_IGNORE}="1"', 'ATTRS{idVendor}=="2341", ENV{ID_MM_DEVICE_IGNORE}="1"'},
99 'tmk': {_deprecated_udev_rule("feed")}
100 }
101
102 if any(udev_dir.exists() for udev_dir in udev_dirs):
103 udev_rules = [rule_file for udev_dir in udev_dirs for rule_file in udev_dir.glob('*.rules')]
104 current_rules = set()
105
106 # Collect all rules from the config files
107 for rule_file in udev_rules:
108 for line in rule_file.read_text(encoding='utf-8').split('\n'):
109 line = line.strip()
110 if not line.startswith("#") and len(line):
111 current_rules.add(line)
112
113 # Check if the desired rules are among the currently present rules
114 for bootloader, rules in desired_rules.items():
115 if not rules.issubset(current_rules):
116 deprecated_rule = deprecated_rules.get(bootloader)
117 if deprecated_rule and deprecated_rule.issubset(current_rules):
118 cli.log.warning("{fg_yellow}Found old, deprecated udev rules for '%s' boards. The new rules on https://docs.qmk.fm/#/faq_build?id=linux-udev-rules offer better security with the same functionality.", bootloader)
119 else:
120 # For caterina, check if ModemManager is running
121 if bootloader == "caterina":
122 if check_modem_manager():
123 rc = CheckStatus.WARNING
124 cli.log.warning("{fg_yellow}Detected ModemManager without the necessary udev rules. Please either disable it or set the appropriate udev rules if you are using a Pro Micro.")
125 rc = CheckStatus.WARNING
126 cli.log.warning("{fg_yellow}Missing or outdated udev rules for '%s' boards. Run 'sudo cp %s/util/udev/50-qmk.rules /etc/udev/rules.d/'.", bootloader, QMK_FIRMWARE)
127
128 else:
129 cli.log.warning("{fg_yellow}Can't find udev rules, skipping udev rule checking...")
130 cli.log.debug("Checked directories: %s", ', '.join(str(udev_dir) for udev_dir in udev_dirs))
131
132 return rc
133
134
135def check_systemd():
136 """Check if it's a systemd system
137 """
138 return bool(shutil.which("systemctl"))
139
140
141def check_modem_manager():
142 """Returns True if ModemManager is running.
143
144 """
145 if check_systemd():
146 mm_check = cli.run(["systemctl", "--quiet", "is-active", "ModemManager.service"], timeout=10)
147 if mm_check.returncode == 0:
148 return True
149 else:
150 """(TODO): Add check for non-systemd systems
151 """
152 return False
153
154
155def os_test_linux():
156 """Run the Linux specific tests.
157 """
158 # Don't bother with udev on WSL, for now
159 if 'microsoft' in platform.uname().release.lower():
160 cli.log.info("Detected {fg_cyan}Linux (WSL){fg_reset}.")
161
162 # https://github.com/microsoft/WSL/issues/4197
163 if QMK_FIRMWARE.as_posix().startswith("/mnt"):
164 cli.log.warning("I/O performance on /mnt may be extremely slow.")
165 return CheckStatus.WARNING
166
167 return CheckStatus.OK
168 else:
169 cli.log.info("Detected {fg_cyan}Linux{fg_reset}.")
170 from .linux import check_udev_rules
171
172 return check_udev_rules()
diff --git a/lib/python/qmk/cli/doctor/macos.py b/lib/python/qmk/cli/doctor/macos.py
new file mode 100644
index 000000000..00fb27285
--- /dev/null
+++ b/lib/python/qmk/cli/doctor/macos.py
@@ -0,0 +1,13 @@
1import platform
2
3from milc import cli
4
5from .check import CheckStatus
6
7
8def os_test_macos():
9 """Run the Mac specific tests.
10 """
11 cli.log.info("Detected {fg_cyan}macOS %s{fg_reset}.", platform.mac_ver()[0])
12
13 return CheckStatus.OK
diff --git a/lib/python/qmk/cli/doctor.py b/lib/python/qmk/cli/doctor/main.py
index 327bc9cb3..6a31ccdfd 100755
--- a/lib/python/qmk/cli/doctor.py
+++ b/lib/python/qmk/cli/doctor/main.py
@@ -7,9 +7,11 @@ from subprocess import DEVNULL
7 7
8from milc import cli 8from milc import cli
9from milc.questions import yesno 9from milc.questions import yesno
10
10from qmk import submodules 11from qmk import submodules
11from qmk.constants import QMK_FIRMWARE 12from qmk.constants import QMK_FIRMWARE, QMK_FIRMWARE_UPSTREAM
12from qmk.os_helpers import CheckStatus, check_binaries, check_binary_versions, check_submodules, check_git_repo 13from .check import CheckStatus, check_binaries, check_binary_versions, check_submodules
14from qmk.commands import git_check_repo, git_get_branch, git_is_dirty, git_get_remotes, git_check_deviation, in_virtualenv
13 15
14 16
15def os_tests(): 17def os_tests():
@@ -18,51 +20,48 @@ def os_tests():
18 platform_id = platform.platform().lower() 20 platform_id = platform.platform().lower()
19 21
20 if 'darwin' in platform_id or 'macos' in platform_id: 22 if 'darwin' in platform_id or 'macos' in platform_id:
23 from .macos import os_test_macos
21 return os_test_macos() 24 return os_test_macos()
22 elif 'linux' in platform_id: 25 elif 'linux' in platform_id:
26 from .linux import os_test_linux
23 return os_test_linux() 27 return os_test_linux()
24 elif 'windows' in platform_id: 28 elif 'windows' in platform_id:
29 from .windows import os_test_windows
25 return os_test_windows() 30 return os_test_windows()
26 else: 31 else:
27 cli.log.warning('Unsupported OS detected: %s', platform_id) 32 cli.log.warning('Unsupported OS detected: %s', platform_id)
28 return CheckStatus.WARNING 33 return CheckStatus.WARNING
29 34
30 35
31def os_test_linux(): 36def git_tests():
32 """Run the Linux specific tests. 37 """Run Git-related checks
33 """ 38 """
34 # Don't bother with udev on WSL, for now 39 status = CheckStatus.OK
35 if 'microsoft' in platform.uname().release.lower():
36 cli.log.info("Detected {fg_cyan}Linux (WSL){fg_reset}.")
37
38 # https://github.com/microsoft/WSL/issues/4197
39 if QMK_FIRMWARE.as_posix().startswith("/mnt"):
40 cli.log.warning("I/O performance on /mnt may be extremely slow.")
41 return CheckStatus.WARNING
42 40
43 return CheckStatus.OK 41 # Make sure our QMK home is a Git repo
42 git_ok = git_check_repo()
43 if not git_ok:
44 cli.log.warning("{fg_yellow}QMK home does not appear to be a Git repository! (no .git folder)")
45 status = CheckStatus.WARNING
44 else: 46 else:
45 cli.log.info("Detected {fg_cyan}Linux{fg_reset}.") 47 git_branch = git_get_branch()
46 from qmk.os_helpers.linux import check_udev_rules 48 if git_branch:
47 49 cli.log.info('Git branch: %s', git_branch)
48 return check_udev_rules() 50 git_dirty = git_is_dirty()
49 51 if git_dirty:
50 52 cli.log.warning('{fg_yellow}Git has unstashed/uncommitted changes.')
51def os_test_macos(): 53 status = CheckStatus.WARNING
52 """Run the Mac specific tests. 54 git_remotes = git_get_remotes()
53 """ 55 if 'upstream' not in git_remotes.keys() or QMK_FIRMWARE_UPSTREAM not in git_remotes['upstream'].get('url', ''):
54 cli.log.info("Detected {fg_cyan}macOS %s{fg_reset}.", platform.mac_ver()[0]) 56 cli.log.warning('{fg_yellow}The official repository does not seem to be configured as git remote "upstream".')
55 57 status = CheckStatus.WARNING
56 return CheckStatus.OK 58 else:
57 59 git_deviation = git_check_deviation(git_branch)
58 60 if git_branch in ['master', 'develop'] and git_deviation:
59def os_test_windows(): 61 cli.log.warning('{fg_yellow}The local "%s" branch contains commits not found in the upstream branch.', git_branch)
60 """Run the Windows specific tests. 62 status = CheckStatus.WARNING
61 """ 63
62 win32_ver = platform.win32_ver() 64 return status
63 cli.log.info("Detected {fg_cyan}Windows %s (%s){fg_reset}.", win32_ver[0], win32_ver[1])
64
65 return CheckStatus.OK
66 65
67 66
68@cli.argument('-y', '--yes', action='store_true', arg_only=True, help='Answer yes to all questions.') 67@cli.argument('-y', '--yes', action='store_true', arg_only=True, help='Answer yes to all questions.')
@@ -82,12 +81,11 @@ def doctor(cli):
82 81
83 status = os_tests() 82 status = os_tests()
84 83
85 # Make sure our QMK home is a Git repo 84 status = git_tests()
86 git_ok = check_git_repo()
87 85
88 if git_ok == CheckStatus.WARNING: 86 venv = in_virtualenv()
89 cli.log.warning("QMK home does not appear to be a Git repository! (no .git folder)") 87 if venv:
90 status = CheckStatus.WARNING 88 cli.log.info('CLI installed in virtualenv.')
91 89
92 # Make sure the basic CLI tools we need are available and can be executed. 90 # Make sure the basic CLI tools we need are available and can be executed.
93 bin_ok = check_binaries() 91 bin_ok = check_binaries()
diff --git a/lib/python/qmk/cli/doctor/windows.py b/lib/python/qmk/cli/doctor/windows.py
new file mode 100644
index 000000000..381ab36fd
--- /dev/null
+++ b/lib/python/qmk/cli/doctor/windows.py
@@ -0,0 +1,14 @@
1import platform
2
3from milc import cli
4
5from .check import CheckStatus
6
7
8def os_test_windows():
9 """Run the Windows specific tests.
10 """
11 win32_ver = platform.win32_ver()
12 cli.log.info("Detected {fg_cyan}Windows %s (%s){fg_reset}.", win32_ver[0], win32_ver[1])
13
14 return CheckStatus.OK
diff --git a/lib/python/qmk/cli/fileformat.py b/lib/python/qmk/cli/fileformat.py
index 112d8d59d..cee4ba1ac 100644..100755
--- a/lib/python/qmk/cli/fileformat.py
+++ b/lib/python/qmk/cli/fileformat.py
@@ -1,13 +1,23 @@
1"""Format files according to QMK's style. 1"""Point people to the new command name.
2""" 2"""
3from milc import cli 3import sys
4from pathlib import Path
4 5
5import subprocess 6from milc import cli
6 7
7 8
8@cli.subcommand("Format files according to QMK's style.", hidden=True) 9@cli.subcommand('Pointer to the new command name: qmk format-text.', hidden=True)
9def fileformat(cli): 10def fileformat(cli):
10 """Run several general formatting commands. 11 """Pointer to the new command name: qmk format-text.
11 """ 12 """
12 dos2unix = subprocess.run(['bash', '-c', 'git ls-files -z | xargs -0 dos2unix'], stdout=subprocess.DEVNULL) 13 cli.log.warning('"qmk fileformat" has been renamed to "qmk format-text". Please use the new command in the future.')
13 return dos2unix.returncode 14 argv = [sys.executable, *sys.argv]
15 argv[argv.index('fileformat')] = 'format-text'
16 script_path = Path(argv[1])
17 script_path_exe = Path(f'{argv[1]}.exe')
18
19 if not script_path.exists() and script_path_exe.exists():
20 # For reasons I don't understand ".exe" is stripped from the script name on windows.
21 argv[1] = str(script_path_exe)
22
23 return cli.run(argv, capture_output=False).returncode
diff --git a/lib/python/qmk/cli/flash.py b/lib/python/qmk/cli/flash.py
index 1b2932a5b..c2d9e09c6 100644
--- a/lib/python/qmk/cli/flash.py
+++ b/lib/python/qmk/cli/flash.py
@@ -38,7 +38,7 @@ def print_bootloader_help():
38@cli.argument('-km', '--keymap', help='The keymap to build a firmware for. Use this if you dont have a configurator file. Ignored when a configurator file is supplied.') 38@cli.argument('-km', '--keymap', help='The keymap to build a firmware for. Use this if you dont have a configurator file. Ignored when a configurator file is supplied.')
39@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='The keyboard to build a firmware for. Use this if you dont have a configurator file. Ignored when a configurator file is supplied.') 39@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='The keyboard to build a firmware for. Use this if you dont have a configurator file. Ignored when a configurator file is supplied.')
40@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually build, just show the make command to be run.") 40@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually build, just show the make command to be run.")
41@cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs to run.") 41@cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs; 0 means unlimited.")
42@cli.argument('-e', '--env', arg_only=True, action='append', default=[], help="Set a variable to be passed to make. May be passed multiple times.") 42@cli.argument('-e', '--env', arg_only=True, action='append', default=[], help="Set a variable to be passed to make. May be passed multiple times.")
43@cli.argument('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.") 43@cli.argument('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.")
44@cli.subcommand('QMK Flash.') 44@cli.subcommand('QMK Flash.')
diff --git a/lib/python/qmk/cli/format/c.py b/lib/python/qmk/cli/format/c.py
new file mode 100644
index 000000000..b7263e19f
--- /dev/null
+++ b/lib/python/qmk/cli/format/c.py
@@ -0,0 +1,137 @@
1"""Format C code according to QMK's style.
2"""
3from os import path
4from shutil import which
5from subprocess import CalledProcessError, DEVNULL, Popen, PIPE
6
7from argcomplete.completers import FilesCompleter
8from milc import cli
9
10from qmk.path import normpath
11from qmk.c_parse import c_source_files
12
13c_file_suffixes = ('c', 'h', 'cpp')
14core_dirs = ('drivers', 'quantum', 'tests', 'tmk_core', 'platforms')
15ignored = ('tmk_core/protocol/usb_hid', 'quantum/template', 'platforms/chibios')
16
17
18def find_clang_format():
19 """Returns the path to clang-format.
20 """
21 for clang_version in range(20, 6, -1):
22 binary = f'clang-format-{clang_version}'
23
24 if which(binary):
25 return binary
26
27 return 'clang-format'
28
29
30def find_diffs(files):
31 """Run clang-format and diff it against a file.
32 """
33 found_diffs = False
34
35 for file in files:
36 cli.log.debug('Checking for changes in %s', file)
37 clang_format = Popen([find_clang_format(), file], stdout=PIPE, stderr=PIPE, universal_newlines=True)
38 diff = cli.run(['diff', '-u', f'--label=a/{file}', f'--label=b/{file}', str(file), '-'], stdin=clang_format.stdout, capture_output=True)
39
40 if diff.returncode != 0:
41 print(diff.stdout)
42 found_diffs = True
43
44 return found_diffs
45
46
47def cformat_run(files):
48 """Spawn clang-format subprocess with proper arguments
49 """
50 # Determine which version of clang-format to use
51 clang_format = [find_clang_format(), '-i']
52
53 try:
54 cli.run([*clang_format, *map(str, files)], check=True, capture_output=False, stdin=DEVNULL)
55 cli.log.info('Successfully formatted the C code.')
56 return True
57
58 except CalledProcessError as e:
59 cli.log.error('Error formatting C code!')
60 cli.log.debug('%s exited with returncode %s', e.cmd, e.returncode)
61 cli.log.debug('STDOUT:')
62 cli.log.debug(e.stdout)
63 cli.log.debug('STDERR:')
64 cli.log.debug(e.stderr)
65 return False
66
67
68def filter_files(files, core_only=False):
69 """Yield only files to be formatted and skip the rest
70 """
71 if core_only:
72 # Filter non-core files
73 for index, file in enumerate(files):
74 # The following statement checks each file to see if the file path is
75 # - in the core directories
76 # - not in the ignored directories
77 if not any(i in str(file) for i in core_dirs) or any(i in str(file) for i in ignored):
78 files[index] = None
79 cli.log.debug("Skipping non-core file %s, as '--core-only' is used.", file)
80
81 for file in files:
82 if file and file.name.split('.')[-1] in c_file_suffixes:
83 yield file
84 else:
85 cli.log.debug('Skipping file %s', file)
86
87
88@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Flag only, don't automatically format.")
89@cli.argument('-b', '--base-branch', default='origin/master', help='Branch to compare to diffs to.')
90@cli.argument('-a', '--all-files', arg_only=True, action='store_true', help='Format all core files.')
91@cli.argument('--core-only', arg_only=True, action='store_true', help='Format core files only.')
92@cli.argument('files', nargs='*', arg_only=True, type=normpath, completer=FilesCompleter('.c'), help='Filename(s) to format.')
93@cli.subcommand("Format C code according to QMK's style.", hidden=False if cli.config.user.developer else True)
94def format_c(cli):
95 """Format C code according to QMK's style.
96 """
97 # Find the list of files to format
98 if cli.args.files:
99 files = list(filter_files(cli.args.files, cli.args.core_only))
100
101 if not files:
102 cli.log.error('No C files in filelist: %s', ', '.join(map(str, cli.args.files)))
103 exit(0)
104
105 if cli.args.all_files:
106 cli.log.warning('Filenames passed with -a, only formatting: %s', ','.join(map(str, files)))
107
108 elif cli.args.all_files:
109 all_files = c_source_files(core_dirs)
110 files = list(filter_files(all_files, True))
111
112 else:
113 git_diff_cmd = ['git', 'diff', '--name-only', cli.args.base_branch, *core_dirs]
114 git_diff = cli.run(git_diff_cmd, stdin=DEVNULL)
115
116 if git_diff.returncode != 0:
117 cli.log.error("Error running %s", git_diff_cmd)
118 print(git_diff.stderr)
119 return git_diff.returncode
120
121 files = []
122
123 for file in git_diff.stdout.strip().split('\n'):
124 if not any([file.startswith(ignore) for ignore in ignored]):
125 if path.exists(file) and file.split('.')[-1] in c_file_suffixes:
126 files.append(file)
127
128 # Sanity check
129 if not files:
130 cli.log.error('No changed files detected. Use "qmk format-c -a" to format all core files')
131 return False
132
133 # Run clang-format on the files we've found
134 if cli.args.dry_run:
135 return not find_diffs(files)
136 else:
137 return cformat_run(files)
diff --git a/lib/python/qmk/cli/format/json.py b/lib/python/qmk/cli/format/json.py
index 1358c70e7..19d504491 100755
--- a/lib/python/qmk/cli/format/json.py
+++ b/lib/python/qmk/cli/format/json.py
@@ -8,7 +8,7 @@ from jsonschema import ValidationError
8from milc import cli 8from milc import cli
9 9
10from qmk.info import info_json 10from qmk.info import info_json
11from qmk.json_schema import json_load, keyboard_validate 11from qmk.json_schema import json_load, validate
12from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder 12from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder
13from qmk.path import normpath 13from qmk.path import normpath
14 14
@@ -23,14 +23,13 @@ def format_json(cli):
23 23
24 if cli.args.format == 'auto': 24 if cli.args.format == 'auto':
25 try: 25 try:
26 keyboard_validate(json_file) 26 validate(json_file, 'qmk.keyboard.v1')
27 json_encoder = InfoJSONEncoder 27 json_encoder = InfoJSONEncoder
28 28
29 except ValidationError as e: 29 except ValidationError as e:
30 cli.log.warning('File %s did not validate as a keyboard:\n\t%s', cli.args.json_file, e) 30 cli.log.warning('File %s did not validate as a keyboard:\n\t%s', cli.args.json_file, e)
31 cli.log.info('Treating %s as a keymap file.', cli.args.json_file) 31 cli.log.info('Treating %s as a keymap file.', cli.args.json_file)
32 json_encoder = KeymapJSONEncoder 32 json_encoder = KeymapJSONEncoder
33
34 elif cli.args.format == 'keyboard': 33 elif cli.args.format == 'keyboard':
35 json_encoder = InfoJSONEncoder 34 json_encoder = InfoJSONEncoder
36 elif cli.args.format == 'keymap': 35 elif cli.args.format == 'keymap':
diff --git a/lib/python/qmk/cli/format/python.py b/lib/python/qmk/cli/format/python.py
new file mode 100755
index 000000000..00612f97e
--- /dev/null
+++ b/lib/python/qmk/cli/format/python.py
@@ -0,0 +1,26 @@
1"""Format python code according to QMK's style.
2"""
3from subprocess import CalledProcessError, DEVNULL
4
5from milc import cli
6
7
8@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually format.")
9@cli.subcommand("Format python code according to QMK's style.", hidden=False if cli.config.user.developer else True)
10def format_python(cli):
11 """Format python code according to QMK's style.
12 """
13 edit = '--diff' if cli.args.dry_run else '--in-place'
14 yapf_cmd = ['yapf', '-vv', '--recursive', edit, 'bin/qmk', 'lib/python']
15 try:
16 cli.run(yapf_cmd, check=True, capture_output=False, stdin=DEVNULL)
17 cli.log.info('Python code in `bin/qmk` and `lib/python` is correctly formatted.')
18 return True
19
20 except CalledProcessError:
21 if cli.args.dry_run:
22 cli.log.error('Python code in `bin/qmk` and `lib/python` incorrectly formatted!')
23 else:
24 cli.log.error('Error formatting python code!')
25
26 return False
diff --git a/lib/python/qmk/cli/format/text.py b/lib/python/qmk/cli/format/text.py
new file mode 100644
index 000000000..e7e07b729
--- /dev/null
+++ b/lib/python/qmk/cli/format/text.py
@@ -0,0 +1,27 @@
1"""Ensure text files have the proper line endings.
2"""
3from subprocess import CalledProcessError
4
5from milc import cli
6
7
8@cli.subcommand("Ensure text files have the proper line endings.", hidden=True)
9def format_text(cli):
10 """Ensure text files have the proper line endings.
11 """
12 try:
13 file_list_cmd = cli.run(['git', 'ls-files', '-z'], check=True)
14 except CalledProcessError as e:
15 cli.log.error('Could not get file list: %s', e)
16 exit(1)
17 except Exception as e:
18 cli.log.error('Unhandled exception: %s: %s', e.__class__.__name__, e)
19 cli.log.exception(e)
20 exit(1)
21
22 dos2unix = cli.run(['xargs', '-0', 'dos2unix'], stdin=None, input=file_list_cmd.stdout)
23
24 if dos2unix.returncode != 0:
25 print(dos2unix.stderr)
26
27 return dos2unix.returncode
diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py
index 54cd5b96a..c0c148f1c 100755
--- a/lib/python/qmk/cli/generate/config_h.py
+++ b/lib/python/qmk/cli/generate/config_h.py
@@ -12,7 +12,7 @@ from qmk.keyboard import keyboard_completer, keyboard_folder
12from qmk.path import is_keyboard, normpath 12from qmk.path import is_keyboard, normpath
13 13
14 14
15def direct_pins(direct_pins): 15def direct_pins(direct_pins, postfix):
16 """Return the config.h lines that set the direct pins. 16 """Return the config.h lines that set the direct pins.
17 """ 17 """
18 rows = [] 18 rows = []
@@ -24,81 +24,60 @@ def direct_pins(direct_pins):
24 col_count = len(direct_pins[0]) 24 col_count = len(direct_pins[0])
25 row_count = len(direct_pins) 25 row_count = len(direct_pins)
26 26
27 return """ 27 return f"""
28#ifndef MATRIX_COLS 28#ifndef MATRIX_COLS{postfix}
29# define MATRIX_COLS %s 29# define MATRIX_COLS{postfix} {col_count}
30#endif // MATRIX_COLS 30#endif // MATRIX_COLS{postfix}
31 31
32#ifndef MATRIX_ROWS 32#ifndef MATRIX_ROWS{postfix}
33# define MATRIX_ROWS %s 33# define MATRIX_ROWS{postfix} {row_count}
34#endif // MATRIX_ROWS 34#endif // MATRIX_ROWS{postfix}
35 35
36#ifndef DIRECT_PINS 36#ifndef DIRECT_PINS{postfix}
37# define DIRECT_PINS {%s} 37# define DIRECT_PINS{postfix} {{ {", ".join(rows)} }}
38#endif // DIRECT_PINS 38#endif // DIRECT_PINS{postfix}
39""" % (col_count, row_count, ','.join(rows)) 39"""
40 40
41 41
42def pin_array(define, pins): 42def pin_array(define, pins, postfix):
43 """Return the config.h lines that set a pin array. 43 """Return the config.h lines that set a pin array.
44 """ 44 """
45 pin_num = len(pins) 45 pin_num = len(pins)
46 pin_array = ', '.join(map(str, [pin or 'NO_PIN' for pin in pins])) 46 pin_array = ', '.join(map(str, [pin or 'NO_PIN' for pin in pins]))
47 47
48 return f""" 48 return f"""
49#ifndef {define}S 49#ifndef {define}S{postfix}
50# define {define}S {pin_num} 50# define {define}S{postfix} {pin_num}
51#endif // {define}S 51#endif // {define}S{postfix}
52 52
53#ifndef {define}_PINS 53#ifndef {define}_PINS{postfix}
54# define {define}_PINS {{ {pin_array} }} 54# define {define}_PINS{postfix} {{ {pin_array} }}
55#endif // {define}_PINS 55#endif // {define}_PINS{postfix}
56""" 56"""
57 57
58 58
59def matrix_pins(matrix_pins): 59def matrix_pins(matrix_pins, postfix=''):
60 """Add the matrix config to the config.h. 60 """Add the matrix config to the config.h.
61 """ 61 """
62 pins = [] 62 pins = []
63 63
64 if 'direct' in matrix_pins: 64 if 'direct' in matrix_pins:
65 pins.append(direct_pins(matrix_pins['direct'])) 65 pins.append(direct_pins(matrix_pins['direct'], postfix))
66 66
67 if 'cols' in matrix_pins: 67 if 'cols' in matrix_pins:
68 pins.append(pin_array('MATRIX_COL', matrix_pins['cols'])) 68 pins.append(pin_array('MATRIX_COL', matrix_pins['cols'], postfix))
69 69
70 if 'rows' in matrix_pins: 70 if 'rows' in matrix_pins:
71 pins.append(pin_array('MATRIX_ROW', matrix_pins['rows'])) 71 pins.append(pin_array('MATRIX_ROW', matrix_pins['rows'], postfix))
72 72
73 return '\n'.join(pins) 73 return '\n'.join(pins)
74 74
75 75
76@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to') 76def generate_config_items(kb_info_json, config_h_lines):
77@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") 77 """Iterate through the info_config map to generate basic config values.
78@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate config.h for.')
79@cli.subcommand('Used by the make system to generate info_config.h from info.json', hidden=True)
80@automagic_keyboard
81@automagic_keymap
82def generate_config_h(cli):
83 """Generates the info_config.h file.
84 """ 78 """
85 # Determine our keyboard(s)
86 if not cli.config.generate_config_h.keyboard:
87 cli.log.error('Missing parameter: --keyboard')
88 cli.subcommands['info'].print_help()
89 return False
90
91 if not is_keyboard(cli.config.generate_config_h.keyboard):
92 cli.log.error('Invalid keyboard: "%s"', cli.config.generate_config_h.keyboard)
93 return False
94
95 # Build the info_config.h file.
96 kb_info_json = dotty(info_json(cli.config.generate_config_h.keyboard))
97 info_config_map = json_load(Path('data/mappings/info_config.json')) 79 info_config_map = json_load(Path('data/mappings/info_config.json'))
98 80
99 config_h_lines = ['/* This file was generated by `qmk generate-config-h`. Do not edit or copy.' ' */', '', '#pragma once']
100
101 # Iterate through the info_config map to generate basic things
102 for config_key, info_dict in info_config_map.items(): 81 for config_key, info_dict in info_config_map.items():
103 info_key = info_dict['info_key'] 82 info_key = info_dict['info_key']
104 key_type = info_dict.get('value_type', 'str') 83 key_type = info_dict.get('value_type', 'str')
@@ -135,9 +114,78 @@ def generate_config_h(cli):
135 config_h_lines.append(f'# define {config_key} {config_value}') 114 config_h_lines.append(f'# define {config_key} {config_value}')
136 config_h_lines.append(f'#endif // {config_key}') 115 config_h_lines.append(f'#endif // {config_key}')
137 116
117
118def generate_split_config(kb_info_json, config_h_lines):
119 """Generate the config.h lines for split boards."""
120 if 'primary' in kb_info_json['split']:
121 if kb_info_json['split']['primary'] in ('left', 'right'):
122 config_h_lines.append('')
123 config_h_lines.append('#ifndef MASTER_LEFT')
124 config_h_lines.append('# ifndef MASTER_RIGHT')
125 if kb_info_json['split']['primary'] == 'left':
126 config_h_lines.append('# define MASTER_LEFT')
127 elif kb_info_json['split']['primary'] == 'right':
128 config_h_lines.append('# define MASTER_RIGHT')
129 config_h_lines.append('# endif // MASTER_RIGHT')
130 config_h_lines.append('#endif // MASTER_LEFT')
131 elif kb_info_json['split']['primary'] == 'pin':
132 config_h_lines.append('')
133 config_h_lines.append('#ifndef SPLIT_HAND_PIN')
134 config_h_lines.append('# define SPLIT_HAND_PIN')
135 config_h_lines.append('#endif // SPLIT_HAND_PIN')
136 elif kb_info_json['split']['primary'] == 'matrix_grid':
137 config_h_lines.append('')
138 config_h_lines.append('#ifndef SPLIT_HAND_MATRIX_GRID')
139 config_h_lines.append('# define SPLIT_HAND_MATRIX_GRID {%s}' % (','.join(kb_info_json["split"]["matrix_grid"],)))
140 config_h_lines.append('#endif // SPLIT_HAND_MATRIX_GRID')
141 elif kb_info_json['split']['primary'] == 'eeprom':
142 config_h_lines.append('')
143 config_h_lines.append('#ifndef EE_HANDS')
144 config_h_lines.append('# define EE_HANDS')
145 config_h_lines.append('#endif // EE_HANDS')
146
147 if 'protocol' in kb_info_json['split'].get('transport', {}):
148 if kb_info_json['split']['transport']['protocol'] == 'i2c':
149 config_h_lines.append('')
150 config_h_lines.append('#ifndef USE_I2C')
151 config_h_lines.append('# define USE_I2C')
152 config_h_lines.append('#endif // USE_I2C')
153
154 if 'right' in kb_info_json['split'].get('matrix_pins', {}):
155 config_h_lines.append(matrix_pins(kb_info_json['split']['matrix_pins']['right'], '_RIGHT'))
156
157
158@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
159@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
160@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate config.h for.')
161@cli.subcommand('Used by the make system to generate info_config.h from info.json', hidden=True)
162@automagic_keyboard
163@automagic_keymap
164def generate_config_h(cli):
165 """Generates the info_config.h file.
166 """
167 # Determine our keyboard(s)
168 if not cli.config.generate_config_h.keyboard:
169 cli.log.error('Missing parameter: --keyboard')
170 cli.subcommands['info'].print_help()
171 return False
172
173 if not is_keyboard(cli.config.generate_config_h.keyboard):
174 cli.log.error('Invalid keyboard: "%s"', cli.config.generate_config_h.keyboard)
175 return False
176
177 # Build the info_config.h file.
178 kb_info_json = dotty(info_json(cli.config.generate_config_h.keyboard))
179 config_h_lines = ['/* This file was generated by `qmk generate-config-h`. Do not edit or copy.' ' */', '', '#pragma once']
180
181 generate_config_items(kb_info_json, config_h_lines)
182
138 if 'matrix_pins' in kb_info_json: 183 if 'matrix_pins' in kb_info_json:
139 config_h_lines.append(matrix_pins(kb_info_json['matrix_pins'])) 184 config_h_lines.append(matrix_pins(kb_info_json['matrix_pins']))
140 185
186 if 'split' in kb_info_json:
187 generate_split_config(kb_info_json, config_h_lines)
188
141 # Show the results 189 # Show the results
142 config_h = '\n'.join(config_h_lines) 190 config_h = '\n'.join(config_h_lines)
143 191
diff --git a/lib/python/qmk/cli/generate/info_json.py b/lib/python/qmk/cli/generate/info_json.py
index 8931b68b6..284d1a851 100755
--- a/lib/python/qmk/cli/generate/info_json.py
+++ b/lib/python/qmk/cli/generate/info_json.py
@@ -4,15 +4,17 @@ Compile an info.json for a particular keyboard and pretty-print it.
4""" 4"""
5import json 5import json
6 6
7from jsonschema import Draft7Validator, validators 7from argcomplete.completers import FilesCompleter
8from jsonschema import Draft7Validator, RefResolver, validators
8from milc import cli 9from milc import cli
10from pathlib import Path
9 11
10from qmk.decorators import automagic_keyboard, automagic_keymap 12from qmk.decorators import automagic_keyboard, automagic_keymap
11from qmk.info import info_json 13from qmk.info import info_json
12from qmk.json_encoders import InfoJSONEncoder 14from qmk.json_encoders import InfoJSONEncoder
13from qmk.json_schema import load_jsonschema 15from qmk.json_schema import compile_schema_store
14from qmk.keyboard import keyboard_completer, keyboard_folder 16from qmk.keyboard import keyboard_completer, keyboard_folder
15from qmk.path import is_keyboard 17from qmk.path import is_keyboard, normpath
16 18
17 19
18def pruning_validator(validator_class): 20def pruning_validator(validator_class):
@@ -34,15 +36,19 @@ def pruning_validator(validator_class):
34def strip_info_json(kb_info_json): 36def strip_info_json(kb_info_json):
35 """Remove the API-only properties from the info.json. 37 """Remove the API-only properties from the info.json.
36 """ 38 """
39 schema_store = compile_schema_store()
37 pruning_draft_7_validator = pruning_validator(Draft7Validator) 40 pruning_draft_7_validator = pruning_validator(Draft7Validator)
38 schema = load_jsonschema('keyboard') 41 schema = schema_store['qmk.keyboard.v1']
39 validator = pruning_draft_7_validator(schema).validate 42 resolver = RefResolver.from_schema(schema_store['qmk.keyboard.v1'], store=schema_store)
43 validator = pruning_draft_7_validator(schema, resolver=resolver).validate
40 44
41 return validator(kb_info_json) 45 return validator(kb_info_json)
42 46
43 47
44@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Keyboard to show info for.') 48@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Keyboard to show info for.')
45@cli.argument('-km', '--keymap', help='Show the layers for a JSON keymap too.') 49@cli.argument('-km', '--keymap', help='Show the layers for a JSON keymap too.')
50@cli.argument('-o', '--output', arg_only=True, completer=FilesCompleter, help='Write the output the specified file, overwriting if necessary.')
51@cli.argument('-ow', '--overwrite', arg_only=True, action='store_true', help='Overwrite the existing info.json. (Overrides the location of --output)')
46@cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True) 52@cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True)
47@automagic_keyboard 53@automagic_keyboard
48@automagic_keymap 54@automagic_keymap
@@ -59,9 +65,29 @@ def generate_info_json(cli):
59 cli.log.error('Invalid keyboard: "%s"', cli.config.generate_info_json.keyboard) 65 cli.log.error('Invalid keyboard: "%s"', cli.config.generate_info_json.keyboard)
60 return False 66 return False
61 67
68 if cli.args.overwrite:
69 output_path = (Path('keyboards') / cli.config.generate_info_json.keyboard / 'info.json').resolve()
70
71 if cli.args.output:
72 cli.log.warning('Overwriting user supplied --output with %s', output_path)
73
74 cli.args.output = output_path
75
62 # Build the info.json file 76 # Build the info.json file
63 kb_info_json = info_json(cli.config.generate_info_json.keyboard) 77 kb_info_json = info_json(cli.config.generate_info_json.keyboard)
64 strip_info_json(kb_info_json) 78 strip_info_json(kb_info_json)
79 info_json_text = json.dumps(kb_info_json, indent=4, cls=InfoJSONEncoder)
80
81 if cli.args.output:
82 # Write to a file
83 output_path = normpath(cli.args.output)
84
85 if output_path.exists():
86 cli.log.warning('Overwriting output file %s', output_path)
87
88 output_path.write_text(info_json_text + '\n')
89 cli.log.info('Wrote info.json to %s.', output_path)
65 90
66 # Display the results 91 else:
67 print(json.dumps(kb_info_json, indent=2, cls=InfoJSONEncoder)) 92 # Display the results
93 print(info_json_text)
diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py
index 41c94e16b..2712b81cb 100755
--- a/lib/python/qmk/cli/generate/rules_mk.py
+++ b/lib/python/qmk/cli/generate/rules_mk.py
@@ -76,6 +76,17 @@ def generate_rules_mk(cli):
76 enabled = 'yes' if enabled else 'no' 76 enabled = 'yes' if enabled else 'no'
77 rules_mk_lines.append(f'{feature}_ENABLE ?= {enabled}') 77 rules_mk_lines.append(f'{feature}_ENABLE ?= {enabled}')
78 78
79 # Set SPLIT_TRANSPORT, if needed
80 if kb_info_json.get('split', {}).get('transport', {}).get('protocol') == 'custom':
81 rules_mk_lines.append('SPLIT_TRANSPORT ?= custom')
82
83 # Set CUSTOM_MATRIX, if needed
84 if kb_info_json.get('matrix_pins', {}).get('custom'):
85 if kb_info_json.get('matrix_pins', {}).get('custom_lite'):
86 rules_mk_lines.append('CUSTOM_MATRIX ?= lite')
87 else:
88 rules_mk_lines.append('CUSTOM_MATRIX ?= yes')
89
79 # Show the results 90 # Show the results
80 rules_mk = '\n'.join(rules_mk_lines) + '\n' 91 rules_mk = '\n'.join(rules_mk_lines) + '\n'
81 92
diff --git a/lib/python/qmk/cli/generate/version_h.py b/lib/python/qmk/cli/generate/version_h.py
new file mode 100644
index 000000000..b8e52588c
--- /dev/null
+++ b/lib/python/qmk/cli/generate/version_h.py
@@ -0,0 +1,28 @@
1"""Used by the make system to generate version.h for use in code.
2"""
3from milc import cli
4
5from qmk.commands import create_version_h
6from qmk.path import normpath
7
8
9@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
10@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
11@cli.argument('--skip-git', arg_only=True, action='store_true', help='Skip Git operations')
12@cli.argument('--skip-all', arg_only=True, action='store_true', help='Use placeholder values for all defines (implies --skip-git)')
13@cli.subcommand('Used by the make system to generate version.h for use in code', hidden=True)
14def generate_version_h(cli):
15 """Generates the version.h file.
16 """
17 if cli.args.skip_all:
18 cli.args.skip_git = True
19
20 version_h = create_version_h(cli.args.skip_git, cli.args.skip_all)
21
22 if cli.args.output:
23 cli.args.output.write_text(version_h)
24
25 if not cli.args.quiet:
26 cli.log.info('Wrote version.h to %s.', cli.args.output)
27 else:
28 print(version_h)
diff --git a/lib/python/qmk/cli/info.py b/lib/python/qmk/cli/info.py
index 0d08d242c..3131d4b53 100755
--- a/lib/python/qmk/cli/info.py
+++ b/lib/python/qmk/cli/info.py
@@ -24,19 +24,15 @@ def show_keymap(kb_info_json, title_caps=True):
24 keymap_path = locate_keymap(cli.config.info.keyboard, cli.config.info.keymap) 24 keymap_path = locate_keymap(cli.config.info.keyboard, cli.config.info.keymap)
25 25
26 if keymap_path and keymap_path.suffix == '.json': 26 if keymap_path and keymap_path.suffix == '.json':
27 if title_caps:
28 cli.echo('{fg_blue}Keymap "%s"{fg_reset}:', cli.config.info.keymap)
29 else:
30 cli.echo('{fg_blue}keymap_%s{fg_reset}:', cli.config.info.keymap)
31
32 keymap_data = json.load(keymap_path.open(encoding='utf-8')) 27 keymap_data = json.load(keymap_path.open(encoding='utf-8'))
33 layout_name = keymap_data['layout'] 28 layout_name = keymap_data['layout']
29 layout_name = kb_info_json.get('layout_aliases', {}).get(layout_name, layout_name) # Resolve alias names
34 30
35 for layer_num, layer in enumerate(keymap_data['layers']): 31 for layer_num, layer in enumerate(keymap_data['layers']):
36 if title_caps: 32 if title_caps:
37 cli.echo('{fg_cyan}Layer %s{fg_reset}:', layer_num) 33 cli.echo('{fg_cyan}Keymap %s Layer %s{fg_reset}:', cli.config.info.keymap, layer_num)
38 else: 34 else:
39 cli.echo('{fg_cyan}layer_%s{fg_reset}:', layer_num) 35 cli.echo('{fg_cyan}keymap.%s.layer.%s{fg_reset}:', cli.config.info.keymap, layer_num)
40 36
41 print(render_layout(kb_info_json['layouts'][layout_name]['layout'], cli.config.info.ascii, layer)) 37 print(render_layout(kb_info_json['layouts'][layout_name]['layout'], cli.config.info.ascii, layer))
42 38
@@ -45,7 +41,7 @@ def show_layouts(kb_info_json, title_caps=True):
45 """Render the layouts with info.json labels. 41 """Render the layouts with info.json labels.
46 """ 42 """
47 for layout_name, layout_art in render_layouts(kb_info_json, cli.config.info.ascii).items(): 43 for layout_name, layout_art in render_layouts(kb_info_json, cli.config.info.ascii).items():
48 title = layout_name.title() if title_caps else layout_name 44 title = f'Layout {layout_name.title()}' if title_caps else f'layouts.{layout_name}'
49 cli.echo('{fg_cyan}%s{fg_reset}:', title) 45 cli.echo('{fg_cyan}%s{fg_reset}:', title)
50 print(layout_art) # Avoid passing dirty data to cli.echo() 46 print(layout_art) # Avoid passing dirty data to cli.echo()
51 47
@@ -87,23 +83,12 @@ def print_friendly_output(kb_info_json):
87 cli.echo('{fg_blue}Maintainer{fg_reset}: %s', kb_info_json['maintainer']) 83 cli.echo('{fg_blue}Maintainer{fg_reset}: %s', kb_info_json['maintainer'])
88 cli.echo('{fg_blue}Keyboard Folder{fg_reset}: %s', kb_info_json.get('keyboard_folder', 'Unknown')) 84 cli.echo('{fg_blue}Keyboard Folder{fg_reset}: %s', kb_info_json.get('keyboard_folder', 'Unknown'))
89 cli.echo('{fg_blue}Layouts{fg_reset}: %s', ', '.join(sorted(kb_info_json['layouts'].keys()))) 85 cli.echo('{fg_blue}Layouts{fg_reset}: %s', ', '.join(sorted(kb_info_json['layouts'].keys())))
90 if 'width' in kb_info_json and 'height' in kb_info_json:
91 cli.echo('{fg_blue}Size{fg_reset}: %s x %s' % (kb_info_json['width'], kb_info_json['height']))
92 cli.echo('{fg_blue}Processor{fg_reset}: %s', kb_info_json.get('processor', 'Unknown')) 86 cli.echo('{fg_blue}Processor{fg_reset}: %s', kb_info_json.get('processor', 'Unknown'))
93 cli.echo('{fg_blue}Bootloader{fg_reset}: %s', kb_info_json.get('bootloader', 'Unknown')) 87 cli.echo('{fg_blue}Bootloader{fg_reset}: %s', kb_info_json.get('bootloader', 'Unknown'))
94 if 'layout_aliases' in kb_info_json: 88 if 'layout_aliases' in kb_info_json:
95 aliases = [f'{key}={value}' for key, value in kb_info_json['layout_aliases'].items()] 89 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),)) 90 cli.echo('{fg_blue}Layout aliases:{fg_reset} %s' % (', '.join(aliases),))
97 91
98 if cli.config.info.layouts:
99 show_layouts(kb_info_json, True)
100
101 if cli.config.info.matrix:
102 show_matrix(kb_info_json, True)
103
104 if cli.config_source.info.keymap and cli.config_source.info.keymap != 'config_file':
105 show_keymap(kb_info_json, True)
106
107 92
108def print_text_output(kb_info_json): 93def print_text_output(kb_info_json):
109 """Print the info.json in a plain text format. 94 """Print the info.json in a plain text format.
@@ -124,6 +109,24 @@ def print_text_output(kb_info_json):
124 show_keymap(kb_info_json, False) 109 show_keymap(kb_info_json, False)
125 110
126 111
112def print_dotted_output(kb_info_json, prefix=''):
113 """Print the info.json in a plain text format with dot-joined keys.
114 """
115 for key in sorted(kb_info_json):
116 new_prefix = f'{prefix}.{key}' if prefix else key
117
118 if key in ['parse_errors', 'parse_warnings']:
119 continue
120 elif key == 'layouts' and prefix == '':
121 cli.echo('{fg_blue}layouts{fg_reset}: %s', ', '.join(sorted(kb_info_json['layouts'].keys())))
122 elif isinstance(kb_info_json[key], dict):
123 print_dotted_output(kb_info_json[key], new_prefix)
124 elif isinstance(kb_info_json[key], list):
125 cli.echo('{fg_blue}%s{fg_reset}: %s', new_prefix, ', '.join(map(str, sorted(kb_info_json[key]))))
126 else:
127 cli.echo('{fg_blue}%s{fg_reset}: %s', new_prefix, kb_info_json[key])
128
129
127def print_parsed_rules_mk(keyboard_name): 130def print_parsed_rules_mk(keyboard_name):
128 rules = rules_mk(keyboard_name) 131 rules = rules_mk(keyboard_name)
129 for k in sorted(rules.keys()): 132 for k in sorted(rules.keys()):
@@ -164,10 +167,22 @@ def info(cli):
164 # Output in the requested format 167 # Output in the requested format
165 if cli.args.format == 'json': 168 if cli.args.format == 'json':
166 print(json.dumps(kb_info_json, cls=InfoJSONEncoder)) 169 print(json.dumps(kb_info_json, cls=InfoJSONEncoder))
170 return True
167 elif cli.args.format == 'text': 171 elif cli.args.format == 'text':
168 print_text_output(kb_info_json) 172 print_dotted_output(kb_info_json)
173 title_caps = False
169 elif cli.args.format == 'friendly': 174 elif cli.args.format == 'friendly':
170 print_friendly_output(kb_info_json) 175 print_friendly_output(kb_info_json)
176 title_caps = True
171 else: 177 else:
172 cli.log.error('Unknown format: %s', cli.args.format) 178 cli.log.error('Unknown format: %s', cli.args.format)
173 return False 179 return False
180
181 if cli.config.info.layouts:
182 show_layouts(kb_info_json, title_caps)
183
184 if cli.config.info.matrix:
185 show_matrix(kb_info_json, title_caps)
186
187 if cli.config_source.info.keymap and cli.config_source.info.keymap != 'config_file':
188 show_keymap(kb_info_json, title_caps)
diff --git a/lib/python/qmk/cli/kle2json.py b/lib/python/qmk/cli/kle2json.py
index acb75ef4f..bbfddf426 100755
--- a/lib/python/qmk/cli/kle2json.py
+++ b/lib/python/qmk/cli/kle2json.py
@@ -44,8 +44,6 @@ def kle2json(cli):
44 'keyboard_name': kle.name, 44 'keyboard_name': kle.name,
45 'url': '', 45 'url': '',
46 'maintainer': 'qmk', 46 'maintainer': 'qmk',
47 'width': kle.columns,
48 'height': kle.rows,
49 'layouts': { 47 'layouts': {
50 'LAYOUT': { 48 'LAYOUT': {
51 'layout': kle2qmk(kle) 49 'layout': kle2qmk(kle)
diff --git a/lib/python/qmk/cli/multibuild.py b/lib/python/qmk/cli/multibuild.py
index bdb0b493c..85ed0fa1e 100755
--- a/lib/python/qmk/cli/multibuild.py
+++ b/lib/python/qmk/cli/multibuild.py
@@ -10,7 +10,7 @@ from subprocess import DEVNULL
10from milc import cli 10from milc import cli
11 11
12from qmk.constants import QMK_FIRMWARE 12from qmk.constants import QMK_FIRMWARE
13from qmk.commands import _find_make 13from qmk.commands import _find_make, get_make_parallel_args
14import qmk.keyboard 14import qmk.keyboard
15import qmk.keymap 15import qmk.keymap
16 16
@@ -28,7 +28,7 @@ def _is_split(keyboard_name):
28 return True if 'SPLIT_KEYBOARD' in rules_mk and rules_mk['SPLIT_KEYBOARD'].lower() == 'yes' else False 28 return True if 'SPLIT_KEYBOARD' in rules_mk and rules_mk['SPLIT_KEYBOARD'].lower() == 'yes' else False
29 29
30 30
31@cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs to run.") 31@cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs; 0 means unlimited.")
32@cli.argument('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.") 32@cli.argument('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.")
33@cli.argument('-f', '--filter', arg_only=True, action='append', default=[], help="Filter the list of keyboards based on the supplied value in rules.mk. Supported format is 'SPLIT_KEYBOARD=yes'. May be passed multiple times.") 33@cli.argument('-f', '--filter', arg_only=True, action='append', default=[], help="Filter the list of keyboards based on the supplied value in rules.mk. Supported format is 'SPLIT_KEYBOARD=yes'. May be passed multiple times.")
34@cli.argument('-km', '--keymap', type=str, default='default', help="The keymap name to build. Default is 'default'.") 34@cli.argument('-km', '--keymap', type=str, default='default', help="The keymap name to build. Default is 'default'.")
@@ -80,7 +80,7 @@ all: {keyboard_safe}_binary
80 ) 80 )
81 # yapf: enable 81 # yapf: enable
82 82
83 cli.run([make_cmd, '-j', str(cli.args.parallel), '-f', makefile.as_posix(), 'all'], capture_output=False, stdin=DEVNULL) 83 cli.run([make_cmd, *get_make_parallel_args(cli.args.parallel), '-f', makefile.as_posix(), 'all'], capture_output=False, stdin=DEVNULL)
84 84
85 # Check for failures 85 # Check for failures
86 failures = [f for f in builddir.glob(f'failed.log.{os.getpid()}.*')] 86 failures = [f for f in builddir.glob(f'failed.log.{os.getpid()}.*')]
diff --git a/lib/python/qmk/cli/new/keyboard.py b/lib/python/qmk/cli/new/keyboard.py
index ae4445ca4..9e4232679 100644
--- a/lib/python/qmk/cli/new/keyboard.py
+++ b/lib/python/qmk/cli/new/keyboard.py
@@ -1,11 +1,142 @@
1"""This script automates the creation of keyboards. 1"""This script automates the creation of new keyboard directories using a starter template.
2""" 2"""
3from datetime import date
4import fileinput
5from pathlib import Path
6import re
7import shutil
8
9from qmk.commands import git_get_username
10import qmk.path
3from milc import cli 11from milc import cli
12from milc.questions import choice, question
13
14KEYBOARD_TYPES = ['avr', 'ps2avrgb']
15
16
17def keyboard_name(name):
18 """Callable for argparse validation.
19 """
20 if not validate_keyboard_name(name):
21 raise ValueError
22 return name
4 23
5 24
6@cli.subcommand('Creates a new keyboard') 25def validate_keyboard_name(name):
26 """Returns True if the given keyboard name contains only lowercase a-z, 0-9 and underscore characters.
27 """
28 regex = re.compile(r'^[a-z0-9][a-z0-9/_]+$')
29 return bool(regex.match(name))
30
31
32@cli.argument('-kb', '--keyboard', help='Specify the name for the new keyboard directory', arg_only=True, type=keyboard_name)
33@cli.argument('-t', '--type', help='Specify the keyboard type', arg_only=True, choices=KEYBOARD_TYPES)
34@cli.argument('-u', '--username', help='Specify your username (default from Git config)', arg_only=True)
35@cli.subcommand('Creates a new keyboard directory')
7def new_keyboard(cli): 36def new_keyboard(cli):
8 """Creates a new keyboard 37 """Creates a new keyboard.
9 """ 38 """
10 # TODO: replace this bodge to the existing script 39 cli.log.info('{style_bright}Generating a new QMK keyboard directory{style_normal}')
11 cli.run(['util/new_keyboard.sh'], stdin=None, capture_output=False) 40 cli.echo('')
41
42 # Get keyboard name
43 new_keyboard_name = None
44 while not new_keyboard_name:
45 new_keyboard_name = cli.args.keyboard if cli.args.keyboard else question('Keyboard Name:')
46 if not validate_keyboard_name(new_keyboard_name):
47 cli.log.error('Keyboard names must contain only {fg_cyan}lowercase a-z{fg_reset}, {fg_cyan}0-9{fg_reset}, and {fg_cyan}_{fg_reset}! Please choose a different name.')
48
49 # Exit if passed by arg
50 if cli.args.keyboard:
51 return False
52
53 new_keyboard_name = None
54 continue
55
56 keyboard_path = qmk.path.keyboard(new_keyboard_name)
57 if keyboard_path.exists():
58 cli.log.error(f'Keyboard {{fg_cyan}}{new_keyboard_name}{{fg_reset}} already exists! Please choose a different name.')
59
60 # Exit if passed by arg
61 if cli.args.keyboard:
62 return False
63
64 new_keyboard_name = None
65
66 # Get keyboard type
67 keyboard_type = cli.args.type if cli.args.type else choice('Keyboard Type:', KEYBOARD_TYPES, default=0)
68
69 # Get username
70 user_name = None
71 while not user_name:
72 user_name = question('Your Name:', default=find_user_name())
73
74 if not user_name:
75 cli.log.error('You didn\'t provide a username, and we couldn\'t find one set in your QMK or Git configs. Please try again.')
76
77 # Exit if passed by arg
78 if cli.args.username:
79 return False
80
81 # Copy all the files
82 copy_templates(keyboard_type, keyboard_path)
83
84 # Replace all the placeholders
85 keyboard_basename = keyboard_path.name
86 replacements = [
87 ('%YEAR%', str(date.today().year)),
88 ('%KEYBOARD%', keyboard_basename),
89 ('%YOUR_NAME%', user_name),
90 ]
91 filenames = [
92 keyboard_path / 'config.h',
93 keyboard_path / 'info.json',
94 keyboard_path / 'readme.md',
95 keyboard_path / f'{keyboard_basename}.c',
96 keyboard_path / f'{keyboard_basename}.h',
97 keyboard_path / 'keymaps/default/readme.md',
98 keyboard_path / 'keymaps/default/keymap.c',
99 ]
100 replace_placeholders(replacements, filenames)
101
102 cli.echo('')
103 cli.log.info(f'{{fg_green}}Created a new keyboard called {{fg_cyan}}{new_keyboard_name}{{fg_green}}.{{fg_reset}}')
104 cli.log.info(f'To start working on things, `cd` into {{fg_cyan}}{keyboard_path}{{fg_reset}},')
105 cli.log.info('or open the directory in your preferred text editor.')
106
107
108def find_user_name():
109 if cli.args.username:
110 return cli.args.username
111 elif cli.config.user.name:
112 return cli.config.user.name
113 else:
114 return git_get_username()
115
116
117def copy_templates(keyboard_type, keyboard_path):
118 """Copies the template files from quantum/template to the new keyboard directory.
119 """
120 template_base_path = Path('quantum/template')
121 keyboard_basename = keyboard_path.name
122
123 cli.log.info('Copying base template files...')
124 shutil.copytree(template_base_path / 'base', keyboard_path)
125
126 cli.log.info(f'Copying {{fg_cyan}}{keyboard_type}{{fg_reset}} template files...')
127 shutil.copytree(template_base_path / keyboard_type, keyboard_path, dirs_exist_ok=True)
128
129 cli.log.info(f'Renaming {{fg_cyan}}keyboard.[ch]{{fg_reset}} to {{fg_cyan}}{keyboard_basename}.[ch]{{fg_reset}}...')
130 shutil.move(keyboard_path / 'keyboard.c', keyboard_path / f'{keyboard_basename}.c')
131 shutil.move(keyboard_path / 'keyboard.h', keyboard_path / f'{keyboard_basename}.h')
132
133
134def replace_placeholders(replacements, filenames):
135 """Replaces the given placeholders in each template file.
136 """
137 for replacement in replacements:
138 cli.log.info(f'Replacing {{fg_cyan}}{replacement[0]}{{fg_reset}} with {{fg_cyan}}{replacement[1]}{{fg_reset}}...')
139
140 with fileinput.input(files=filenames, inplace=True) as file:
141 for line in file:
142 print(line.replace(replacement[0], replacement[1]), end='')
diff --git a/lib/python/qmk/cli/pyformat.py b/lib/python/qmk/cli/pyformat.py
index abe5f6de1..c624f74ae 100755
--- a/lib/python/qmk/cli/pyformat.py
+++ b/lib/python/qmk/cli/pyformat.py
@@ -1,26 +1,24 @@
1"""Format python code according to QMK's style. 1"""Point people to the new command name.
2""" 2"""
3from subprocess import CalledProcessError, DEVNULL 3import sys
4from pathlib import Path
4 5
5from milc import cli 6from milc import cli
6 7
7 8
8@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Flag only, don't automatically format.") 9@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually format.")
9@cli.subcommand("Format python code according to QMK's style.", hidden=False if cli.config.user.developer else True) 10@cli.subcommand('Pointer to the new command name: qmk format-python.', hidden=False if cli.config.user.developer else True)
10def pyformat(cli): 11def pyformat(cli):
11 """Format python code according to QMK's style. 12 """Pointer to the new command name: qmk format-python.
12 """ 13 """
13 edit = '--diff' if cli.args.dry_run else '--in-place' 14 cli.log.warning('"qmk pyformat" has been renamed to "qmk format-python". Please use the new command in the future.')
14 yapf_cmd = ['yapf', '-vv', '--recursive', edit, 'bin/qmk', 'lib/python'] 15 argv = [sys.executable, *sys.argv]
15 try: 16 argv[argv.index('pyformat')] = 'format-python'
16 cli.run(yapf_cmd, check=True, capture_output=False, stdin=DEVNULL) 17 script_path = Path(argv[1])
17 cli.log.info('Python code in `bin/qmk` and `lib/python` is correctly formatted.') 18 script_path_exe = Path(f'{argv[1]}.exe')
18 return True
19 19
20 except CalledProcessError: 20 if not script_path.exists() and script_path_exe.exists():
21 if cli.args.dry_run: 21 # For reasons I don't understand ".exe" is stripped from the script name on windows.
22 cli.log.error('Python code in `bin/qmk` and `lib/python` incorrectly formatted!') 22 argv[1] = str(script_path_exe)
23 else:
24 cli.log.error('Error formatting python code!')
25 23
26 return False 24 return cli.run(argv, capture_output=False).returncode