Merge remote-tracking branch 'upstream/develop' into xap
This commit is contained in:
		
						commit
						bf66b91433
					
				
					 5591 changed files with 131128 additions and 54530 deletions
				
			
		| 
						 | 
				
			
			@ -1 +1 @@
 | 
			
		|||
Subproject commit 413e39c5681d181720440f2a8b7391f581788d7b
 | 
			
		||||
Subproject commit d7b9d1c87f724bd7c8cd1486d6d0dc3ba52e0d52
 | 
			
		||||
| 
						 | 
				
			
			@ -31,6 +31,7 @@ safe_commands = [
 | 
			
		|||
subcommands = [
 | 
			
		||||
    'qmk.cli.bux',
 | 
			
		||||
    'qmk.cli.c2json',
 | 
			
		||||
    'qmk.cli.cd',
 | 
			
		||||
    'qmk.cli.cformat',
 | 
			
		||||
    'qmk.cli.chibios.confmigrate',
 | 
			
		||||
    'qmk.cli.clean',
 | 
			
		||||
| 
						 | 
				
			
			@ -44,7 +45,9 @@ subcommands = [
 | 
			
		|||
    'qmk.cli.format.python',
 | 
			
		||||
    'qmk.cli.format.text',
 | 
			
		||||
    'qmk.cli.generate.api',
 | 
			
		||||
    'qmk.cli.generate.compilation_database',
 | 
			
		||||
    'qmk.cli.generate.config_h',
 | 
			
		||||
    'qmk.cli.generate.develop_pr_list',
 | 
			
		||||
    'qmk.cli.generate.dfu_header',
 | 
			
		||||
    'qmk.cli.generate.docs',
 | 
			
		||||
    'qmk.cli.generate.info_json',
 | 
			
		||||
| 
						 | 
				
			
			@ -59,6 +62,7 @@ subcommands = [
 | 
			
		|||
    'qmk.cli.lint',
 | 
			
		||||
    'qmk.cli.list.keyboards',
 | 
			
		||||
    'qmk.cli.list.keymaps',
 | 
			
		||||
    'qmk.cli.list.layouts',
 | 
			
		||||
    'qmk.cli.kle2json',
 | 
			
		||||
    'qmk.cli.multibuild',
 | 
			
		||||
    'qmk.cli.new.keyboard',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										46
									
								
								lib/python/qmk/cli/cd.py
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										46
									
								
								lib/python/qmk/cli/cd.py
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,46 @@
 | 
			
		|||
"""Open a shell in the QMK Home directory
 | 
			
		||||
"""
 | 
			
		||||
import sys
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
from milc import cli
 | 
			
		||||
 | 
			
		||||
from qmk.path import under_qmk_firmware
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@cli.subcommand('Go to QMK Home')
 | 
			
		||||
def cd(cli):
 | 
			
		||||
    """Go to QMK Home
 | 
			
		||||
    """
 | 
			
		||||
    if not sys.stdout.isatty():
 | 
			
		||||
        cli.log.error("This command is for interactive usage only. For non-interactive usage, 'cd $(qmk env QMK_HOME)' is more robust.")
 | 
			
		||||
        sys.exit(1)
 | 
			
		||||
 | 
			
		||||
    if not under_qmk_firmware():
 | 
			
		||||
        # Only do anything if the user is not under qmk_firmware already
 | 
			
		||||
        # in order to reduce the possibility of starting multiple shells
 | 
			
		||||
        cli.log.info("Spawning a subshell in your QMK_HOME directory.")
 | 
			
		||||
        cli.log.info("Type 'exit' to get back to the parent shell.")
 | 
			
		||||
        if not cli.platform.lower().startswith('windows'):
 | 
			
		||||
            # For Linux/Mac/etc
 | 
			
		||||
            # Check the user's login shell from 'passwd'
 | 
			
		||||
            # alternatively fall back to $SHELL env var
 | 
			
		||||
            # and finally to '/bin/bash'.
 | 
			
		||||
            import getpass
 | 
			
		||||
            import pwd
 | 
			
		||||
            shell = pwd.getpwnam(getpass.getuser()).pw_shell
 | 
			
		||||
            if not shell:
 | 
			
		||||
                shell = os.environ.get('SHELL', '/bin/bash')
 | 
			
		||||
            # Start the new subshell
 | 
			
		||||
            os.execl(shell, shell)
 | 
			
		||||
        else:
 | 
			
		||||
            # For Windows
 | 
			
		||||
            # Check the $SHELL env var
 | 
			
		||||
            # and fall back to '/usr/bin/bash'.
 | 
			
		||||
            qmk_env = os.environ.copy()
 | 
			
		||||
            # Set the prompt for the new shell
 | 
			
		||||
            qmk_env['MSYS2_PS1'] = qmk_env['PS1']
 | 
			
		||||
            # Start the new subshell
 | 
			
		||||
            cli.run([os.environ.get('SHELL', '/usr/bin/bash')], env=qmk_env)
 | 
			
		||||
    else:
 | 
			
		||||
        cli.log.info("Already within qmk_firmware directory.")
 | 
			
		||||
| 
						 | 
				
			
			@ -2,6 +2,7 @@
 | 
			
		|||
"""
 | 
			
		||||
import http.server
 | 
			
		||||
import os
 | 
			
		||||
import shutil
 | 
			
		||||
import webbrowser
 | 
			
		||||
 | 
			
		||||
from milc import cli
 | 
			
		||||
| 
						 | 
				
			
			@ -11,20 +12,33 @@ from milc import cli
 | 
			
		|||
@cli.argument('-b', '--browser', action='store_true', help='Open the docs in the default browser.')
 | 
			
		||||
@cli.subcommand('Run a local webserver for QMK documentation.', hidden=False if cli.config.user.developer else True)
 | 
			
		||||
def docs(cli):
 | 
			
		||||
    """Spin up a local HTTPServer instance for the QMK docs.
 | 
			
		||||
    """Spin up a local HTTP server for the QMK docs.
 | 
			
		||||
    """
 | 
			
		||||
    os.chdir('docs')
 | 
			
		||||
 | 
			
		||||
    with http.server.HTTPServer(('', cli.config.docs.port), http.server.SimpleHTTPRequestHandler) as httpd:
 | 
			
		||||
        cli.log.info(f"Serving QMK docs at http://localhost:{cli.config.docs.port}/")
 | 
			
		||||
    # If docsify-cli is installed, run that instead so we get live reload
 | 
			
		||||
    if shutil.which('docsify'):
 | 
			
		||||
        command = ['docsify', 'serve', '--port', f'{cli.config.docs.port}', '--open' if cli.config.docs.browser else '']
 | 
			
		||||
 | 
			
		||||
        cli.log.info(f"Running {{fg_cyan}}{str.join(' ', command)}{{fg_reset}}")
 | 
			
		||||
        cli.log.info("Press Control+C to exit.")
 | 
			
		||||
 | 
			
		||||
        if cli.config.docs.browser:
 | 
			
		||||
            webbrowser.open(f'http://localhost:{cli.config.docs.port}')
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            httpd.serve_forever()
 | 
			
		||||
            cli.run(command, capture_output=False)
 | 
			
		||||
        except KeyboardInterrupt:
 | 
			
		||||
            cli.log.info("Stopping HTTP server...")
 | 
			
		||||
        finally:
 | 
			
		||||
            httpd.shutdown()
 | 
			
		||||
    else:
 | 
			
		||||
        # Fall back to Python HTTPServer
 | 
			
		||||
        with http.server.HTTPServer(('', cli.config.docs.port), http.server.SimpleHTTPRequestHandler) as httpd:
 | 
			
		||||
            cli.log.info(f"Serving QMK docs at http://localhost:{cli.config.docs.port}/")
 | 
			
		||||
            cli.log.info("Press Control+C to exit.")
 | 
			
		||||
 | 
			
		||||
            if cli.config.docs.browser:
 | 
			
		||||
                webbrowser.open(f'http://localhost:{cli.config.docs.port}')
 | 
			
		||||
 | 
			
		||||
            try:
 | 
			
		||||
                httpd.serve_forever()
 | 
			
		||||
            except KeyboardInterrupt:
 | 
			
		||||
                cli.log.info("Stopping HTTP server...")
 | 
			
		||||
            finally:
 | 
			
		||||
                httpd.shutdown()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -118,10 +118,9 @@ def check_udev_rules():
 | 
			
		|||
                    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)
 | 
			
		||||
                else:
 | 
			
		||||
                    # For caterina, check if ModemManager is running
 | 
			
		||||
                    if bootloader == "caterina":
 | 
			
		||||
                        if check_modem_manager():
 | 
			
		||||
                            rc = CheckStatus.WARNING
 | 
			
		||||
                            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.")
 | 
			
		||||
                    if bootloader == "caterina" and check_modem_manager():
 | 
			
		||||
                        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.")
 | 
			
		||||
 | 
			
		||||
                    rc = CheckStatus.WARNING
 | 
			
		||||
                    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)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -167,6 +166,5 @@ def os_test_linux():
 | 
			
		|||
        return CheckStatus.OK
 | 
			
		||||
    else:
 | 
			
		||||
        cli.log.info("Detected {fg_cyan}Linux{fg_reset}.")
 | 
			
		||||
        from .linux import check_udev_rules
 | 
			
		||||
 | 
			
		||||
        return check_udev_rules()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -79,12 +79,13 @@ def doctor(cli):
 | 
			
		|||
    cli.log.info('CLI version: %s', cli.version)
 | 
			
		||||
    cli.log.info('QMK home: {fg_cyan}%s', QMK_FIRMWARE)
 | 
			
		||||
 | 
			
		||||
    status = os_tests()
 | 
			
		||||
    status = os_status = os_tests()
 | 
			
		||||
    git_status = git_tests()
 | 
			
		||||
 | 
			
		||||
    status = git_tests()
 | 
			
		||||
    if git_status == CheckStatus.ERROR or (os_status == CheckStatus.OK and git_status == CheckStatus.WARNING):
 | 
			
		||||
        status = git_status
 | 
			
		||||
 | 
			
		||||
    venv = in_virtualenv()
 | 
			
		||||
    if venv:
 | 
			
		||||
    if in_virtualenv():
 | 
			
		||||
        cli.log.info('CLI installed in virtualenv.')
 | 
			
		||||
 | 
			
		||||
    # Make sure the basic CLI tools we need are available and can be executed.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,17 +18,21 @@ def print_bootloader_help():
 | 
			
		|||
    """Prints the available bootloaders listed in docs.qmk.fm.
 | 
			
		||||
    """
 | 
			
		||||
    cli.log.info('Here are the available bootloaders:')
 | 
			
		||||
    cli.echo('\tavrdude')
 | 
			
		||||
    cli.echo('\tbootloadhid')
 | 
			
		||||
    cli.echo('\tdfu')
 | 
			
		||||
    cli.echo('\tdfu-util')
 | 
			
		||||
    cli.echo('\tmdloader')
 | 
			
		||||
    cli.echo('\tst-flash')
 | 
			
		||||
    cli.echo('\tst-link-cli')
 | 
			
		||||
    cli.log.info('Enhanced variants for split keyboards:')
 | 
			
		||||
    cli.echo('\tavrdude-split-left')
 | 
			
		||||
    cli.echo('\tavrdude-split-right')
 | 
			
		||||
    cli.echo('\tdfu-ee')
 | 
			
		||||
    cli.echo('\tdfu-split-left')
 | 
			
		||||
    cli.echo('\tdfu-split-right')
 | 
			
		||||
    cli.echo('\tavrdude')
 | 
			
		||||
    cli.echo('\tBootloadHID')
 | 
			
		||||
    cli.echo('\tdfu-util')
 | 
			
		||||
    cli.echo('\tdfu-util-split-left')
 | 
			
		||||
    cli.echo('\tdfu-util-split-right')
 | 
			
		||||
    cli.echo('\tst-link-cli')
 | 
			
		||||
    cli.echo('\tst-flash')
 | 
			
		||||
    cli.echo('For more info, visit https://docs.qmk.fm/#/flashing')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,23 +4,66 @@ from subprocess import CalledProcessError, DEVNULL
 | 
			
		|||
 | 
			
		||||
from milc import cli
 | 
			
		||||
 | 
			
		||||
from qmk.path import normpath
 | 
			
		||||
 | 
			
		||||
py_file_suffixes = ('py',)
 | 
			
		||||
py_dirs = ['lib/python']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def yapf_run(files):
 | 
			
		||||
    edit = '--diff' if cli.args.dry_run else '--in-place'
 | 
			
		||||
    yapf_cmd = ['yapf', '-vv', '--recursive', edit, *files]
 | 
			
		||||
    try:
 | 
			
		||||
        cli.run(yapf_cmd, check=True, capture_output=False, stdin=DEVNULL)
 | 
			
		||||
        cli.log.info('Successfully formatted the python code.')
 | 
			
		||||
 | 
			
		||||
    except CalledProcessError:
 | 
			
		||||
        cli.log.error(f'Python code in {",".join(py_dirs)} incorrectly formatted!')
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def filter_files(files):
 | 
			
		||||
    """Yield only files to be formatted and skip the rest
 | 
			
		||||
    """
 | 
			
		||||
    for file in files:
 | 
			
		||||
        if file and normpath(file).name.split('.')[-1] in py_file_suffixes:
 | 
			
		||||
            yield file
 | 
			
		||||
        else:
 | 
			
		||||
            cli.log.debug('Skipping file %s', file)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually format.")
 | 
			
		||||
@cli.argument('-b', '--base-branch', default='origin/master', help='Branch to compare to diffs to.')
 | 
			
		||||
@cli.argument('-a', '--all-files', arg_only=True, action='store_true', help='Format all files.')
 | 
			
		||||
@cli.argument('files', nargs='*', arg_only=True, type=normpath, help='Filename(s) to format.')
 | 
			
		||||
@cli.subcommand("Format python code according to QMK's style.", hidden=False if cli.config.user.developer else True)
 | 
			
		||||
def format_python(cli):
 | 
			
		||||
    """Format python code according to QMK's style.
 | 
			
		||||
    """
 | 
			
		||||
    edit = '--diff' if cli.args.dry_run else '--in-place'
 | 
			
		||||
    yapf_cmd = ['yapf', '-vv', '--recursive', edit, 'lib/python']
 | 
			
		||||
    try:
 | 
			
		||||
        cli.run(yapf_cmd, check=True, capture_output=False, stdin=DEVNULL)
 | 
			
		||||
        cli.log.info('Python code in `lib/python` is correctly formatted.')
 | 
			
		||||
        return True
 | 
			
		||||
    # Find the list of files to format
 | 
			
		||||
    if cli.args.files:
 | 
			
		||||
        files = list(filter_files(cli.args.files))
 | 
			
		||||
 | 
			
		||||
    except CalledProcessError:
 | 
			
		||||
        if cli.args.dry_run:
 | 
			
		||||
            cli.log.error('Python code in `lib/python` is incorrectly formatted!')
 | 
			
		||||
        else:
 | 
			
		||||
            cli.log.error('Error formatting python code!')
 | 
			
		||||
        if not files:
 | 
			
		||||
            cli.log.error('No Python files in filelist: %s', ', '.join(map(str, cli.args.files)))
 | 
			
		||||
            exit(0)
 | 
			
		||||
 | 
			
		||||
    return False
 | 
			
		||||
        if cli.args.all_files:
 | 
			
		||||
            cli.log.warning('Filenames passed with -a, only formatting: %s', ','.join(map(str, files)))
 | 
			
		||||
 | 
			
		||||
    elif cli.args.all_files:
 | 
			
		||||
        git_ls_cmd = ['git', 'ls-files', *py_dirs]
 | 
			
		||||
        git_ls = cli.run(git_ls_cmd, stdin=DEVNULL)
 | 
			
		||||
        files = list(filter_files(git_ls.stdout.split('\n')))
 | 
			
		||||
 | 
			
		||||
    else:
 | 
			
		||||
        git_diff_cmd = ['git', 'diff', '--name-only', cli.args.base_branch, *py_dirs]
 | 
			
		||||
        git_diff = cli.run(git_diff_cmd, stdin=DEVNULL)
 | 
			
		||||
        files = list(filter_files(git_diff.stdout.split('\n')))
 | 
			
		||||
 | 
			
		||||
    # Sanity check
 | 
			
		||||
    if not files:
 | 
			
		||||
        cli.log.error('No changed files detected. Use "qmk format-python -a" to format all files')
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    return yapf_run(files)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,27 +1,57 @@
 | 
			
		|||
"""Ensure text files have the proper line endings.
 | 
			
		||||
"""
 | 
			
		||||
from subprocess import CalledProcessError
 | 
			
		||||
from itertools import islice
 | 
			
		||||
from subprocess import DEVNULL
 | 
			
		||||
 | 
			
		||||
from milc import cli
 | 
			
		||||
 | 
			
		||||
from qmk.path import normpath
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _get_chunks(it, size):
 | 
			
		||||
    """Break down a collection into smaller parts
 | 
			
		||||
    """
 | 
			
		||||
    it = iter(it)
 | 
			
		||||
    return iter(lambda: tuple(islice(it, size)), ())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def dos2unix_run(files):
 | 
			
		||||
    """Spawn multiple dos2unix subprocess avoiding too long commands on formatting everything
 | 
			
		||||
    """
 | 
			
		||||
    for chunk in _get_chunks(files, 10):
 | 
			
		||||
        dos2unix = cli.run(['dos2unix', *chunk])
 | 
			
		||||
 | 
			
		||||
        if dos2unix.returncode:
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@cli.argument('-b', '--base-branch', default='origin/master', help='Branch to compare to diffs to.')
 | 
			
		||||
@cli.argument('-a', '--all-files', arg_only=True, action='store_true', help='Format all files.')
 | 
			
		||||
@cli.argument('files', nargs='*', arg_only=True, type=normpath, help='Filename(s) to format.')
 | 
			
		||||
@cli.subcommand("Ensure text files have the proper line endings.", hidden=True)
 | 
			
		||||
def format_text(cli):
 | 
			
		||||
    """Ensure text files have the proper line endings.
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        file_list_cmd = cli.run(['git', 'ls-files', '-z'], check=True)
 | 
			
		||||
    except CalledProcessError as e:
 | 
			
		||||
        cli.log.error('Could not get file list: %s', e)
 | 
			
		||||
        exit(1)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        cli.log.error('Unhandled exception: %s: %s', e.__class__.__name__, e)
 | 
			
		||||
        cli.log.exception(e)
 | 
			
		||||
        exit(1)
 | 
			
		||||
    # Find the list of files to format
 | 
			
		||||
    if cli.args.files:
 | 
			
		||||
        files = list(cli.args.files)
 | 
			
		||||
 | 
			
		||||
    dos2unix = cli.run(['xargs', '-0', 'dos2unix'], stdin=None, input=file_list_cmd.stdout)
 | 
			
		||||
        if cli.args.all_files:
 | 
			
		||||
            cli.log.warning('Filenames passed with -a, only formatting: %s', ','.join(map(str, files)))
 | 
			
		||||
 | 
			
		||||
    if dos2unix.returncode != 0:
 | 
			
		||||
        print(dos2unix.stderr)
 | 
			
		||||
    elif cli.args.all_files:
 | 
			
		||||
        git_ls_cmd = ['git', 'ls-files']
 | 
			
		||||
        git_ls = cli.run(git_ls_cmd, stdin=DEVNULL)
 | 
			
		||||
        files = list(filter(None, git_ls.stdout.split('\n')))
 | 
			
		||||
 | 
			
		||||
    return dos2unix.returncode
 | 
			
		||||
    else:
 | 
			
		||||
        git_diff_cmd = ['git', 'diff', '--name-only', cli.args.base_branch]
 | 
			
		||||
        git_diff = cli.run(git_diff_cmd, stdin=DEVNULL)
 | 
			
		||||
        files = list(filter(None, git_diff.stdout.split('\n')))
 | 
			
		||||
 | 
			
		||||
    # Sanity check
 | 
			
		||||
    if not files:
 | 
			
		||||
        cli.log.error('No changed files detected. Use "qmk format-text -a" to format all files')
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    return dos2unix_run(files)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										133
									
								
								lib/python/qmk/cli/generate/compilation_database.py
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										133
									
								
								lib/python/qmk/cli/generate/compilation_database.py
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,133 @@
 | 
			
		|||
"""Creates a compilation database for the given keyboard build.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import json
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
import shlex
 | 
			
		||||
import shutil
 | 
			
		||||
from functools import lru_cache
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from typing import Dict, Iterator, List, Union
 | 
			
		||||
 | 
			
		||||
from milc import cli, MILC
 | 
			
		||||
 | 
			
		||||
from qmk.commands import create_make_command
 | 
			
		||||
from qmk.constants import QMK_FIRMWARE
 | 
			
		||||
from qmk.decorators import automagic_keyboard, automagic_keymap
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@lru_cache(maxsize=10)
 | 
			
		||||
def system_libs(binary: str) -> List[Path]:
 | 
			
		||||
    """Find the system include directory that the given build tool uses.
 | 
			
		||||
    """
 | 
			
		||||
    cli.log.debug("searching for system library directory for binary: %s", binary)
 | 
			
		||||
    bin_path = shutil.which(binary)
 | 
			
		||||
 | 
			
		||||
    # Actually query xxxxxx-gcc to find its include paths.
 | 
			
		||||
    if binary.endswith("gcc") or binary.endswith("g++"):
 | 
			
		||||
        result = cli.run([binary, '-E', '-Wp,-v', '-'], capture_output=True, check=True, input='\n')
 | 
			
		||||
        paths = []
 | 
			
		||||
        for line in result.stderr.splitlines():
 | 
			
		||||
            if line.startswith(" "):
 | 
			
		||||
                paths.append(Path(line.strip()).resolve())
 | 
			
		||||
        return paths
 | 
			
		||||
 | 
			
		||||
    return list(Path(bin_path).resolve().parent.parent.glob("*/include")) if bin_path else []
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
file_re = re.compile(r'printf "Compiling: ([^"]+)')
 | 
			
		||||
cmd_re = re.compile(r'LOG=\$\((.+?)&&')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def parse_make_n(f: Iterator[str]) -> List[Dict[str, str]]:
 | 
			
		||||
    """parse the output of `make -n <target>`
 | 
			
		||||
 | 
			
		||||
    This function makes many assumptions about the format of your build log.
 | 
			
		||||
    This happens to work right now for qmk.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    state = 'start'
 | 
			
		||||
    this_file = None
 | 
			
		||||
    records = []
 | 
			
		||||
    for line in f:
 | 
			
		||||
        if state == 'start':
 | 
			
		||||
            m = file_re.search(line)
 | 
			
		||||
            if m:
 | 
			
		||||
                this_file = m.group(1)
 | 
			
		||||
                state = 'cmd'
 | 
			
		||||
 | 
			
		||||
        if state == 'cmd':
 | 
			
		||||
            assert this_file
 | 
			
		||||
            m = cmd_re.search(line)
 | 
			
		||||
            if m:
 | 
			
		||||
                # we have a hit!
 | 
			
		||||
                this_cmd = m.group(1)
 | 
			
		||||
                args = shlex.split(this_cmd)
 | 
			
		||||
                for s in system_libs(args[0]):
 | 
			
		||||
                    args += ['-isystem', '%s' % s]
 | 
			
		||||
                new_cmd = ' '.join(shlex.quote(s) for s in args if s != '-mno-thumb-interwork')
 | 
			
		||||
                records.append({"directory": str(QMK_FIRMWARE.resolve()), "command": new_cmd, "file": this_file})
 | 
			
		||||
                state = 'start'
 | 
			
		||||
 | 
			
		||||
    return records
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@cli.argument('-kb', '--keyboard', help='The keyboard to build a firmware for. Ignored when a configurator export is supplied.')
 | 
			
		||||
@cli.argument('-km', '--keymap', help='The keymap to build a firmware for. Ignored when a configurator export is supplied.')
 | 
			
		||||
@cli.subcommand('Create a compilation database.')
 | 
			
		||||
@automagic_keyboard
 | 
			
		||||
@automagic_keymap
 | 
			
		||||
def generate_compilation_database(cli: MILC) -> Union[bool, int]:
 | 
			
		||||
    """Creates a compilation database for the given keyboard build.
 | 
			
		||||
 | 
			
		||||
    Does a make clean, then a make -n for this target and uses the dry-run output to create
 | 
			
		||||
    a compilation database (compile_commands.json). This file can help some IDEs and
 | 
			
		||||
    IDE-like editors work better. For more information about this:
 | 
			
		||||
 | 
			
		||||
        https://clang.llvm.org/docs/JSONCompilationDatabase.html
 | 
			
		||||
    """
 | 
			
		||||
    command = None
 | 
			
		||||
    # check both config domains: the magic decorator fills in `generate_compilation_database` but the user is
 | 
			
		||||
    # more likely to have set `compile` in their config file.
 | 
			
		||||
    current_keyboard = cli.config.generate_compilation_database.keyboard or cli.config.user.keyboard
 | 
			
		||||
    current_keymap = cli.config.generate_compilation_database.keymap or cli.config.user.keymap
 | 
			
		||||
 | 
			
		||||
    if current_keyboard and current_keymap:
 | 
			
		||||
        # Generate the make command for a specific keyboard/keymap.
 | 
			
		||||
        command = create_make_command(current_keyboard, current_keymap, dry_run=True)
 | 
			
		||||
    elif not current_keyboard:
 | 
			
		||||
        cli.log.error('Could not determine keyboard!')
 | 
			
		||||
    elif not current_keymap:
 | 
			
		||||
        cli.log.error('Could not determine keymap!')
 | 
			
		||||
 | 
			
		||||
    if not command:
 | 
			
		||||
        cli.log.error('You must supply both `--keyboard` and `--keymap`, or be in a directory for a keyboard or keymap.')
 | 
			
		||||
        cli.echo('usage: qmk compiledb [-kb KEYBOARD] [-km KEYMAP]')
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    # remove any environment variable overrides which could trip us up
 | 
			
		||||
    env = os.environ.copy()
 | 
			
		||||
    env.pop("MAKEFLAGS", None)
 | 
			
		||||
 | 
			
		||||
    # re-use same executable as the main make invocation (might be gmake)
 | 
			
		||||
    clean_command = [command[0], 'clean']
 | 
			
		||||
    cli.log.info('Making clean with {fg_cyan}%s', ' '.join(clean_command))
 | 
			
		||||
    cli.run(clean_command, capture_output=False, check=True, env=env)
 | 
			
		||||
 | 
			
		||||
    cli.log.info('Gathering build instructions from {fg_cyan}%s', ' '.join(command))
 | 
			
		||||
 | 
			
		||||
    result = cli.run(command, capture_output=True, check=True, env=env)
 | 
			
		||||
    db = parse_make_n(result.stdout.splitlines())
 | 
			
		||||
    if not db:
 | 
			
		||||
        cli.log.error("Failed to parse output from make output:\n%s", result.stdout)
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    cli.log.info("Found %s compile commands", len(db))
 | 
			
		||||
 | 
			
		||||
    dbpath = QMK_FIRMWARE / 'compile_commands.json'
 | 
			
		||||
 | 
			
		||||
    cli.log.info(f"Writing build database to {dbpath}")
 | 
			
		||||
    dbpath.write_text(json.dumps(db, indent=4))
 | 
			
		||||
 | 
			
		||||
    return True
 | 
			
		||||
| 
						 | 
				
			
			@ -173,7 +173,7 @@ def generate_config_h(cli):
 | 
			
		|||
        kb_info_json = dotty(info_json(cli.args.keyboard))
 | 
			
		||||
 | 
			
		||||
    # Build the info_config.h file.
 | 
			
		||||
    config_h_lines = ['/* This file was generated by `qmk generate-config-h`. Do not edit or copy.' ' */', '', '#pragma once']
 | 
			
		||||
    config_h_lines = ['/* This file was generated by `qmk generate-config-h`. Do not edit or copy.', ' */', '', '#pragma once']
 | 
			
		||||
 | 
			
		||||
    generate_config_items(kb_info_json, config_h_lines)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										119
									
								
								lib/python/qmk/cli/generate/develop_pr_list.py
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										119
									
								
								lib/python/qmk/cli/generate/develop_pr_list.py
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,119 @@
 | 
			
		|||
"""Export the initial list of PRs associated with a `develop` merge to `master`.
 | 
			
		||||
"""
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from subprocess import DEVNULL
 | 
			
		||||
 | 
			
		||||
from milc import cli
 | 
			
		||||
 | 
			
		||||
cache_timeout = 7 * 86400
 | 
			
		||||
fix_expr = re.compile(r'fix', flags=re.IGNORECASE)
 | 
			
		||||
clean1_expr = re.compile(r'\[(develop|keyboard|keymap|core|cli|bug|docs|feature)\]', flags=re.IGNORECASE)
 | 
			
		||||
clean2_expr = re.compile(r'^(develop|keyboard|keymap|core|cli|bug|docs|feature):', flags=re.IGNORECASE)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _get_pr_info(cache, gh, pr_num):
 | 
			
		||||
    pull = cache.get(f'pull:{pr_num}')
 | 
			
		||||
    if pull is None:
 | 
			
		||||
        print(f'Retrieving info for PR #{pr_num}')
 | 
			
		||||
        pull = gh.pulls.get(owner='qmk', repo='qmk_firmware', pull_number=pr_num)
 | 
			
		||||
        cache.set(f'pull:{pr_num}', pull, cache_timeout)
 | 
			
		||||
    return pull
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _try_open_cache(cli):
 | 
			
		||||
    # These dependencies are manually handled because people complain. Fun.
 | 
			
		||||
    try:
 | 
			
		||||
        from sqlite_cache.sqlite_cache import SqliteCache
 | 
			
		||||
    except ImportError:
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    cache_loc = Path(cli.config_file).parent
 | 
			
		||||
    return SqliteCache(cache_loc)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _get_github():
 | 
			
		||||
    try:
 | 
			
		||||
        from ghapi.all import GhApi
 | 
			
		||||
    except ImportError:
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    return GhApi()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@cli.argument('-f', '--from-ref', default='0.11.0', help='Git revision/tag/reference/branch to begin search')
 | 
			
		||||
@cli.argument('-b', '--branch', default='upstream/develop', help='Git branch to iterate (default: "upstream/develop")')
 | 
			
		||||
@cli.subcommand('Creates the develop PR list.', hidden=False if cli.config.user.developer else True)
 | 
			
		||||
def generate_develop_pr_list(cli):
 | 
			
		||||
    """Retrieves information from GitHub regarding the list of PRs associated
 | 
			
		||||
    with a merge of `develop` branch into `master`.
 | 
			
		||||
 | 
			
		||||
    Requires environment variable GITHUB_TOKEN to be set.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    if 'GITHUB_TOKEN' not in os.environ or os.environ['GITHUB_TOKEN'] == '':
 | 
			
		||||
        cli.log.error('Environment variable "GITHUB_TOKEN" is not set.')
 | 
			
		||||
        return 1
 | 
			
		||||
 | 
			
		||||
    cache = _try_open_cache(cli)
 | 
			
		||||
    gh = _get_github()
 | 
			
		||||
 | 
			
		||||
    git_args = ['git', 'rev-list', '--oneline', '--no-merges', '--reverse', f'{cli.args.from_ref}...{cli.args.branch}', '^upstream/master']
 | 
			
		||||
    commit_list = cli.run(git_args, capture_output=True, stdin=DEVNULL)
 | 
			
		||||
 | 
			
		||||
    if cache is None or gh is None:
 | 
			
		||||
        cli.log.error('Missing one or more dependent python packages: "ghapi", "python-sqlite-cache"')
 | 
			
		||||
        return 1
 | 
			
		||||
 | 
			
		||||
    pr_list_bugs = []
 | 
			
		||||
    pr_list_dependencies = []
 | 
			
		||||
    pr_list_core = []
 | 
			
		||||
    pr_list_keyboards = []
 | 
			
		||||
    pr_list_keyboard_fixes = []
 | 
			
		||||
    pr_list_cli = []
 | 
			
		||||
    pr_list_others = []
 | 
			
		||||
 | 
			
		||||
    def _categorise_commit(commit_info):
 | 
			
		||||
        def fix_or_normal(info, fixes_collection, normal_collection):
 | 
			
		||||
            if "bug" in info['pr_labels'] or fix_expr.search(info['title']):
 | 
			
		||||
                fixes_collection.append(info)
 | 
			
		||||
            else:
 | 
			
		||||
                normal_collection.append(info)
 | 
			
		||||
 | 
			
		||||
        if "dependencies" in commit_info['pr_labels']:
 | 
			
		||||
            fix_or_normal(commit_info, pr_list_bugs, pr_list_dependencies)
 | 
			
		||||
        elif "core" in commit_info['pr_labels']:
 | 
			
		||||
            fix_or_normal(commit_info, pr_list_bugs, pr_list_core)
 | 
			
		||||
        elif "keyboard" in commit_info['pr_labels'] or "keymap" in commit_info['pr_labels'] or "via" in commit_info['pr_labels']:
 | 
			
		||||
            fix_or_normal(commit_info, pr_list_keyboard_fixes, pr_list_keyboards)
 | 
			
		||||
        elif "cli" in commit_info['pr_labels']:
 | 
			
		||||
            fix_or_normal(commit_info, pr_list_bugs, pr_list_cli)
 | 
			
		||||
        else:
 | 
			
		||||
            fix_or_normal(commit_info, pr_list_bugs, pr_list_others)
 | 
			
		||||
 | 
			
		||||
    git_expr = re.compile(r'^(?P<hash>[a-f0-9]+) (?P<title>.*) \(#(?P<pr>[0-9]+)\)$')
 | 
			
		||||
    for line in commit_list.stdout.split('\n'):
 | 
			
		||||
        match = git_expr.search(line)
 | 
			
		||||
        if match:
 | 
			
		||||
            pr_info = _get_pr_info(cache, gh, match.group("pr"))
 | 
			
		||||
            commit_info = {'hash': match.group("hash"), 'title': match.group("title"), 'pr_num': int(match.group("pr")), 'pr_labels': [label.name for label in pr_info.labels.items]}
 | 
			
		||||
            _categorise_commit(commit_info)
 | 
			
		||||
 | 
			
		||||
    def _dump_commit_list(name, collection):
 | 
			
		||||
        if len(collection) == 0:
 | 
			
		||||
            return
 | 
			
		||||
        print("")
 | 
			
		||||
        print(f"{name}:")
 | 
			
		||||
        for commit in sorted(collection, key=lambda x: x['pr_num']):
 | 
			
		||||
            title = clean1_expr.sub('', clean2_expr.sub('', commit['title'])).strip()
 | 
			
		||||
            pr_num = commit['pr_num']
 | 
			
		||||
            print(f'* {title} ([#{pr_num}](https://github.com/qmk/qmk_firmware/pull/{pr_num}))')
 | 
			
		||||
 | 
			
		||||
    _dump_commit_list("Core", pr_list_core)
 | 
			
		||||
    _dump_commit_list("CLI", pr_list_cli)
 | 
			
		||||
    _dump_commit_list("Submodule updates", pr_list_dependencies)
 | 
			
		||||
    _dump_commit_list("Keyboards", pr_list_keyboards)
 | 
			
		||||
    _dump_commit_list("Keyboard fixes", pr_list_keyboard_fixes)
 | 
			
		||||
    _dump_commit_list("Others", pr_list_others)
 | 
			
		||||
    _dump_commit_list("Bugs", pr_list_bugs)
 | 
			
		||||
| 
						 | 
				
			
			@ -30,9 +30,9 @@ def generate_dfu_header(cli):
 | 
			
		|||
    # Build the Keyboard.h file.
 | 
			
		||||
    kb_info_json = dotty(info_json(cli.config.generate_dfu_header.keyboard))
 | 
			
		||||
 | 
			
		||||
    keyboard_h_lines = ['/* This file was generated by `qmk generate-dfu-header`. Do not edit or copy.' ' */', '', '#pragma once']
 | 
			
		||||
    keyboard_h_lines = ['/* This file was generated by `qmk generate-dfu-header`. Do not edit or copy.', ' */', '', '#pragma once']
 | 
			
		||||
    keyboard_h_lines.append(f'#define MANUFACTURER {kb_info_json["manufacturer"]}')
 | 
			
		||||
    keyboard_h_lines.append(f'#define PRODUCT {cli.config.generate_dfu_header.keyboard} Bootloader')
 | 
			
		||||
    keyboard_h_lines.append(f'#define PRODUCT {kb_info_json["keyboard_name"]} Bootloader')
 | 
			
		||||
 | 
			
		||||
    # Optional
 | 
			
		||||
    if 'qmk_lufa_bootloader.esc_output' in kb_info_json:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -36,7 +36,7 @@ def generate_keyboard_h(cli):
 | 
			
		|||
    has_layout_h = would_populate_layout_h(cli.args.keyboard)
 | 
			
		||||
 | 
			
		||||
    # Build the layouts.h file.
 | 
			
		||||
    keyboard_h_lines = ['/* This file was generated by `qmk generate-keyboard-h`. Do not edit or copy.' ' */', '', '#pragma once', '#include "quantum.h"']
 | 
			
		||||
    keyboard_h_lines = ['/* This file was generated by `qmk generate-keyboard-h`. Do not edit or copy.', ' */', '', '#pragma once', '#include "quantum.h"']
 | 
			
		||||
 | 
			
		||||
    if not has_layout_h:
 | 
			
		||||
        keyboard_h_lines.append('#pragma error("<keyboard>.h is only optional for data driven keyboards - kb.h == bad times")')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,7 +38,7 @@ def generate_layouts(cli):
 | 
			
		|||
    kb_info_json = info_json(cli.config.generate_layouts.keyboard)
 | 
			
		||||
 | 
			
		||||
    # Build the layouts.h file.
 | 
			
		||||
    layouts_h_lines = ['/* This file was generated by `qmk generate-layouts`. Do not edit or copy.' ' */', '', '#pragma once']
 | 
			
		||||
    layouts_h_lines = ['/* This file was generated by `qmk generate-layouts`. Do not edit or copy.', ' */', '', '#pragma once']
 | 
			
		||||
 | 
			
		||||
    if 'matrix_pins' in kb_info_json:
 | 
			
		||||
        if 'direct' in kb_info_json['matrix_pins']:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,7 +26,7 @@ def process_mapping_rule(kb_info_json, rules_key, info_dict):
 | 
			
		|||
    except KeyError:
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    if key_type == 'array':
 | 
			
		||||
    if key_type in ['array', 'list']:
 | 
			
		||||
        return f'{rules_key} ?= {" ".join(rules_value)}'
 | 
			
		||||
    elif key_type == 'bool':
 | 
			
		||||
        return f'{rules_key} ?= {"on" if rules_value else "off"}'
 | 
			
		||||
| 
						 | 
				
			
			@ -67,12 +67,9 @@ def generate_rules_mk(cli):
 | 
			
		|||
    # Iterate through features to enable/disable them
 | 
			
		||||
    if 'features' in kb_info_json:
 | 
			
		||||
        for feature, enabled in kb_info_json['features'].items():
 | 
			
		||||
            if feature == 'bootmagic_lite' and enabled:
 | 
			
		||||
                rules_mk_lines.append('BOOTMAGIC_ENABLE ?= lite')
 | 
			
		||||
            else:
 | 
			
		||||
                feature = feature.upper()
 | 
			
		||||
                enabled = 'yes' if enabled else 'no'
 | 
			
		||||
                rules_mk_lines.append(f'{feature}_ENABLE ?= {enabled}')
 | 
			
		||||
            feature = feature.upper()
 | 
			
		||||
            enabled = 'yes' if enabled else 'no'
 | 
			
		||||
            rules_mk_lines.append(f'{feature}_ENABLE ?= {enabled}')
 | 
			
		||||
 | 
			
		||||
    # Set SPLIT_TRANSPORT, if needed
 | 
			
		||||
    if kb_info_json.get('split', {}).get('transport', {}).get('protocol') == 'custom':
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,7 +33,7 @@ def json2c(cli):
 | 
			
		|||
        cli.args.output = None
 | 
			
		||||
 | 
			
		||||
    # Generate the keymap
 | 
			
		||||
    keymap_c = qmk.keymap.generate_c(user_keymap['keyboard'], user_keymap['layout'], user_keymap['layers'])
 | 
			
		||||
    keymap_c = qmk.keymap.generate_c(user_keymap)
 | 
			
		||||
 | 
			
		||||
    if cli.args.output:
 | 
			
		||||
        cli.args.output.parent.mkdir(parents=True, exist_ok=True)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,5 +13,10 @@ from qmk.keyboard import keyboard_completer, keyboard_folder
 | 
			
		|||
def list_keymaps(cli):
 | 
			
		||||
    """List the keymaps for a specific keyboard
 | 
			
		||||
    """
 | 
			
		||||
    if not cli.config.list_keymaps.keyboard:
 | 
			
		||||
        cli.log.error('Missing required arguments: --keyboard')
 | 
			
		||||
        cli.subcommands['list-keymaps'].print_help()
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    for name in qmk.keymap.list_keymaps(cli.config.list_keymaps.keyboard):
 | 
			
		||||
        print(name)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										23
									
								
								lib/python/qmk/cli/list/layouts.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								lib/python/qmk/cli/list/layouts.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,23 @@
 | 
			
		|||
"""List the keymaps for a specific keyboard
 | 
			
		||||
"""
 | 
			
		||||
from milc import cli
 | 
			
		||||
 | 
			
		||||
from qmk.decorators import automagic_keyboard
 | 
			
		||||
from qmk.keyboard import keyboard_completer, keyboard_folder
 | 
			
		||||
from qmk.info import info_json
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@cli.argument("-kb", "--keyboard", type=keyboard_folder, completer=keyboard_completer, help="Specify keyboard name. Example: monarch")
 | 
			
		||||
@cli.subcommand("List the layouts for a specific keyboard")
 | 
			
		||||
@automagic_keyboard
 | 
			
		||||
def list_layouts(cli):
 | 
			
		||||
    """List the layouts for a specific keyboard
 | 
			
		||||
    """
 | 
			
		||||
    if not cli.config.list_layouts.keyboard:
 | 
			
		||||
        cli.log.error('Missing required arguments: --keyboard')
 | 
			
		||||
        cli.subcommands['list-layouts'].print_help()
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    info_data = info_json(cli.config.list_layouts.keyboard)
 | 
			
		||||
    for name in sorted(info_data.get('community_layouts', [])):
 | 
			
		||||
        print(name)
 | 
			
		||||
| 
						 | 
				
			
			@ -1,10 +1,8 @@
 | 
			
		|||
"""This script automates the creation of new keyboard directories using a starter template.
 | 
			
		||||
"""
 | 
			
		||||
from datetime import date
 | 
			
		||||
import fileinput
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
import re
 | 
			
		||||
import shutil
 | 
			
		||||
 | 
			
		||||
from qmk.commands import git_get_username
 | 
			
		||||
import qmk.path
 | 
			
		||||
| 
						 | 
				
			
			@ -32,6 +30,7 @@ def validate_keyboard_name(name):
 | 
			
		|||
@cli.argument('-kb', '--keyboard', help='Specify the name for the new keyboard directory', arg_only=True, type=keyboard_name)
 | 
			
		||||
@cli.argument('-t', '--type', help='Specify the keyboard type', arg_only=True, choices=KEYBOARD_TYPES)
 | 
			
		||||
@cli.argument('-u', '--username', help='Specify your username (default from Git config)', arg_only=True)
 | 
			
		||||
@cli.argument('-n', '--realname', help='Specify your real name if you want to use that. Defaults to username', arg_only=True)
 | 
			
		||||
@cli.subcommand('Creates a new keyboard directory')
 | 
			
		||||
def new_keyboard(cli):
 | 
			
		||||
    """Creates a new keyboard.
 | 
			
		||||
| 
						 | 
				
			
			@ -69,7 +68,7 @@ def new_keyboard(cli):
 | 
			
		|||
    # Get username
 | 
			
		||||
    user_name = None
 | 
			
		||||
    while not user_name:
 | 
			
		||||
        user_name = question('Your Name:', default=find_user_name())
 | 
			
		||||
        user_name = question('Your GitHub User Name:', default=find_user_name())
 | 
			
		||||
 | 
			
		||||
        if not user_name:
 | 
			
		||||
            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.')
 | 
			
		||||
| 
						 | 
				
			
			@ -78,26 +77,21 @@ def new_keyboard(cli):
 | 
			
		|||
            if cli.args.username:
 | 
			
		||||
                return False
 | 
			
		||||
 | 
			
		||||
    # Copy all the files
 | 
			
		||||
    copy_templates(keyboard_type, keyboard_path)
 | 
			
		||||
    real_name = None
 | 
			
		||||
    while not real_name:
 | 
			
		||||
        real_name = question('Your real name:', default=user_name)
 | 
			
		||||
 | 
			
		||||
    # Replace all the placeholders
 | 
			
		||||
    keyboard_basename = keyboard_path.name
 | 
			
		||||
    replacements = [
 | 
			
		||||
        ('%YEAR%', str(date.today().year)),
 | 
			
		||||
        ('%KEYBOARD%', keyboard_basename),
 | 
			
		||||
        ('%YOUR_NAME%', user_name),
 | 
			
		||||
    ]
 | 
			
		||||
    filenames = [
 | 
			
		||||
        keyboard_path / 'config.h',
 | 
			
		||||
        keyboard_path / 'info.json',
 | 
			
		||||
        keyboard_path / 'readme.md',
 | 
			
		||||
        keyboard_path / f'{keyboard_basename}.c',
 | 
			
		||||
        keyboard_path / f'{keyboard_basename}.h',
 | 
			
		||||
        keyboard_path / 'keymaps/default/readme.md',
 | 
			
		||||
        keyboard_path / 'keymaps/default/keymap.c',
 | 
			
		||||
    ]
 | 
			
		||||
    replace_placeholders(replacements, filenames)
 | 
			
		||||
    replacements = {
 | 
			
		||||
        "YEAR": str(date.today().year),
 | 
			
		||||
        "KEYBOARD": keyboard_basename,
 | 
			
		||||
        "USER_NAME": user_name,
 | 
			
		||||
        "YOUR_NAME": real_name,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    template_dir = Path('data/templates')
 | 
			
		||||
    template_tree(template_dir / 'base', keyboard_path, replacements)
 | 
			
		||||
    template_tree(template_dir / keyboard_type, keyboard_path, replacements)
 | 
			
		||||
 | 
			
		||||
    cli.echo('')
 | 
			
		||||
    cli.log.info(f'{{fg_green}}Created a new keyboard called {{fg_cyan}}{new_keyboard_name}{{fg_green}}.{{fg_reset}}')
 | 
			
		||||
| 
						 | 
				
			
			@ -114,29 +108,32 @@ def find_user_name():
 | 
			
		|||
        return git_get_username()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def copy_templates(keyboard_type, keyboard_path):
 | 
			
		||||
    """Copies the template files from data/templates to the new keyboard directory.
 | 
			
		||||
def template_tree(src: Path, dst: Path, replacements: dict):
 | 
			
		||||
    """Recursively copy template and replace placeholders
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        src (Path)
 | 
			
		||||
            The source folder to copy from
 | 
			
		||||
        dst (Path)
 | 
			
		||||
            The destination folder to copy to
 | 
			
		||||
        replacements (dict)
 | 
			
		||||
            a dictionary with "key":"value" pairs to replace.
 | 
			
		||||
 | 
			
		||||
    Raises:
 | 
			
		||||
        FileExistsError
 | 
			
		||||
            When trying to overwrite existing files
 | 
			
		||||
    """
 | 
			
		||||
    template_base_path = Path('data/templates')
 | 
			
		||||
    keyboard_basename = keyboard_path.name
 | 
			
		||||
 | 
			
		||||
    cli.log.info('Copying base template files...')
 | 
			
		||||
    shutil.copytree(template_base_path / 'base', keyboard_path)
 | 
			
		||||
    dst.mkdir(parents=True, exist_ok=True)
 | 
			
		||||
 | 
			
		||||
    cli.log.info(f'Copying {{fg_cyan}}{keyboard_type}{{fg_reset}} template files...')
 | 
			
		||||
    shutil.copytree(template_base_path / keyboard_type, keyboard_path, dirs_exist_ok=True)
 | 
			
		||||
    for child in src.iterdir():
 | 
			
		||||
        if child.is_dir():
 | 
			
		||||
            template_tree(child, dst / child.name, replacements=replacements)
 | 
			
		||||
 | 
			
		||||
    cli.log.info(f'Renaming {{fg_cyan}}keyboard.[ch]{{fg_reset}} to {{fg_cyan}}{keyboard_basename}.[ch]{{fg_reset}}...')
 | 
			
		||||
    shutil.move(keyboard_path / 'keyboard.c', keyboard_path / f'{keyboard_basename}.c')
 | 
			
		||||
    shutil.move(keyboard_path / 'keyboard.h', keyboard_path / f'{keyboard_basename}.h')
 | 
			
		||||
        if child.is_file():
 | 
			
		||||
            file_name = dst / (child.name % replacements)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def replace_placeholders(replacements, filenames):
 | 
			
		||||
    """Replaces the given placeholders in each template file.
 | 
			
		||||
    """
 | 
			
		||||
    for replacement in replacements:
 | 
			
		||||
        cli.log.info(f'Replacing {{fg_cyan}}{replacement[0]}{{fg_reset}} with {{fg_cyan}}{replacement[1]}{{fg_reset}}...')
 | 
			
		||||
 | 
			
		||||
        with fileinput.input(files=filenames, inplace=True) as file:
 | 
			
		||||
            for line in file:
 | 
			
		||||
                print(line.replace(replacement[0], replacement[1]), end='')
 | 
			
		||||
            with file_name.open(mode='x') as dst_f:
 | 
			
		||||
                with child.open() as src_f:
 | 
			
		||||
                    template = src_f.read()
 | 
			
		||||
                    dst_f.write(template % replacements)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,7 +28,7 @@ def _find_make():
 | 
			
		|||
    return make_cmd
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_make_target(target, parallel=1, **env_vars):
 | 
			
		||||
def create_make_target(target, dry_run=False, parallel=1, **env_vars):
 | 
			
		||||
    """Create a make command
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
| 
						 | 
				
			
			@ -36,6 +36,9 @@ def create_make_target(target, parallel=1, **env_vars):
 | 
			
		|||
        target
 | 
			
		||||
            Usually a make rule, such as 'clean' or 'all'.
 | 
			
		||||
 | 
			
		||||
        dry_run
 | 
			
		||||
            make -n -- don't actually build
 | 
			
		||||
 | 
			
		||||
        parallel
 | 
			
		||||
            The number of make jobs to run in parallel
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -52,10 +55,10 @@ def create_make_target(target, parallel=1, **env_vars):
 | 
			
		|||
    for key, value in env_vars.items():
 | 
			
		||||
        env.append(f'{key}={value}')
 | 
			
		||||
 | 
			
		||||
    return [make_cmd, *get_make_parallel_args(parallel), *env, target]
 | 
			
		||||
    return [make_cmd, *(['-n'] if dry_run else []), *get_make_parallel_args(parallel), *env, target]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars):
 | 
			
		||||
def create_make_command(keyboard, keymap, target=None, dry_run=False, parallel=1, **env_vars):
 | 
			
		||||
    """Create a make compile command
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
| 
						 | 
				
			
			@ -69,6 +72,9 @@ def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars):
 | 
			
		|||
        target
 | 
			
		||||
            Usually a bootloader.
 | 
			
		||||
 | 
			
		||||
        dry_run
 | 
			
		||||
            make -n -- don't actually build
 | 
			
		||||
 | 
			
		||||
        parallel
 | 
			
		||||
            The number of make jobs to run in parallel
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -84,7 +90,7 @@ def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars):
 | 
			
		|||
    if target:
 | 
			
		||||
        make_args.append(target)
 | 
			
		||||
 | 
			
		||||
    return create_make_target(':'.join(make_args), parallel, **env_vars)
 | 
			
		||||
    return create_make_target(':'.join(make_args), dry_run=dry_run, parallel=parallel, **env_vars)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_git_version(current_time=None, repo_dir='.', check_dir='.'):
 | 
			
		||||
| 
						 | 
				
			
			@ -187,7 +193,7 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va
 | 
			
		|||
    target = f'{keyboard_filesafe}_{user_keymap["keymap"]}'
 | 
			
		||||
    keyboard_output = Path(f'{KEYBOARD_OUTPUT_PREFIX}{keyboard_filesafe}')
 | 
			
		||||
    keymap_output = Path(f'{keyboard_output}_{user_keymap["keymap"]}')
 | 
			
		||||
    c_text = qmk.keymap.generate_c(user_keymap['keyboard'], user_keymap['layout'], user_keymap['layers'])
 | 
			
		||||
    c_text = qmk.keymap.generate_c(user_keymap)
 | 
			
		||||
    keymap_dir = keymap_output / 'src'
 | 
			
		||||
    keymap_c = keymap_dir / 'keymap.c'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,7 +14,7 @@ QMK_FIRMWARE_UPSTREAM = 'qmk/qmk_firmware'
 | 
			
		|||
MAX_KEYBOARD_SUBFOLDERS = 5
 | 
			
		||||
 | 
			
		||||
# Supported processor types
 | 
			
		||||
CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'MK66F18', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F407', 'STM32F411', 'STM32F446', 'STM32G431', 'STM32G474', 'STM32L412', 'STM32L422', 'STM32L433', 'STM32L443'
 | 
			
		||||
CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'MK66FX1M0', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F407', 'STM32F411', 'STM32F446', 'STM32G431', 'STM32G474', 'STM32L412', 'STM32L422', 'STM32L433', 'STM32L443', 'GD32VF103', 'WB32F3G71'
 | 
			
		||||
LUFA_PROCESSORS = 'at90usb162', 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None
 | 
			
		||||
VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,14 +25,21 @@ def _valid_community_layout(layout):
 | 
			
		|||
    return (Path('layouts/default') / layout).exists()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _remove_newlines_from_labels(layouts):
 | 
			
		||||
    for layout_name, layout_json in layouts.items():
 | 
			
		||||
        for key in layout_json['layout']:
 | 
			
		||||
            if '\n' in key['label']:
 | 
			
		||||
                key['label'] = key['label'].split('\n')[0]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def info_json(keyboard):
 | 
			
		||||
    """Generate the info.json data for a specific keyboard.
 | 
			
		||||
    """
 | 
			
		||||
    cur_dir = Path('keyboards')
 | 
			
		||||
    rules = parse_rules_mk_file(cur_dir / keyboard / 'rules.mk')
 | 
			
		||||
    if 'DEFAULT_FOLDER' in rules:
 | 
			
		||||
        keyboard = rules['DEFAULT_FOLDER']
 | 
			
		||||
        rules = parse_rules_mk_file(cur_dir / keyboard / 'rules.mk', rules)
 | 
			
		||||
    root_rules_mk = parse_rules_mk_file(cur_dir / keyboard / 'rules.mk')
 | 
			
		||||
 | 
			
		||||
    if 'DEFAULT_FOLDER' in root_rules_mk:
 | 
			
		||||
        keyboard = root_rules_mk['DEFAULT_FOLDER']
 | 
			
		||||
 | 
			
		||||
    info_data = {
 | 
			
		||||
        'keyboard_name': str(keyboard),
 | 
			
		||||
| 
						 | 
				
			
			@ -99,6 +106,9 @@ def info_json(keyboard):
 | 
			
		|||
    # Check that the reported matrix size is consistent with the actual matrix size
 | 
			
		||||
    _check_matrix(info_data)
 | 
			
		||||
 | 
			
		||||
    # Remove newline characters from layout labels
 | 
			
		||||
    _remove_newlines_from_labels(layouts)
 | 
			
		||||
 | 
			
		||||
    return info_data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -112,11 +122,6 @@ def _extract_features(info_data, rules):
 | 
			
		|||
    if rules.get('BOOTMAGIC_ENABLE') == 'full':
 | 
			
		||||
        rules['BOOTMAGIC_ENABLE'] = 'on'
 | 
			
		||||
 | 
			
		||||
    # Skip non-boolean features we haven't implemented special handling for
 | 
			
		||||
    for feature in 'HAPTIC_ENABLE', 'QWIIC_ENABLE':
 | 
			
		||||
        if rules.get(feature):
 | 
			
		||||
            del rules[feature]
 | 
			
		||||
 | 
			
		||||
    # Process the rest of the rules as booleans
 | 
			
		||||
    for key, value in rules.items():
 | 
			
		||||
        if key.endswith('_ENABLE'):
 | 
			
		||||
| 
						 | 
				
			
			@ -619,6 +624,8 @@ def arm_processor_rules(info_data, rules):
 | 
			
		|||
    if 'bootloader' not in info_data:
 | 
			
		||||
        if 'STM32' in info_data['processor']:
 | 
			
		||||
            info_data['bootloader'] = 'stm32-dfu'
 | 
			
		||||
        elif 'WB32' in info_data['processor']:
 | 
			
		||||
            info_data['bootloader'] = 'wb32-dfu'
 | 
			
		||||
        else:
 | 
			
		||||
            info_data['bootloader'] = 'unknown'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -691,8 +698,8 @@ def merge_info_jsons(keyboard, info_data):
 | 
			
		|||
 | 
			
		||||
            if layout_name in info_data['layouts']:
 | 
			
		||||
                if len(info_data['layouts'][layout_name]['layout']) != len(layout['layout']):
 | 
			
		||||
                    msg = '%s: %s: Number of elements in info.json does not match! info.json:%s != %s:%s'
 | 
			
		||||
                    _log_error(info_data, msg % (info_data['keyboard_folder'], layout_name, len(layout['layout']), layout_name, len(info_data['layouts'][layout_name]['layout'])))
 | 
			
		||||
                    msg = 'Number of keys for %s does not match! info.json specifies %d keys, C macro specifies %d'
 | 
			
		||||
                    _log_error(info_data, msg % (layout_name, len(layout['layout']), len(info_data['layouts'][layout_name]['layout'])))
 | 
			
		||||
                else:
 | 
			
		||||
                    for new_key, existing_key in zip(layout['layout'], info_data['layouts'][layout_name]['layout']):
 | 
			
		||||
                        existing_key.update(new_key)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,6 +17,7 @@ from qmk.errors import CppError
 | 
			
		|||
 | 
			
		||||
# The `keymap.c` template to use when a keyboard doesn't have its own
 | 
			
		||||
DEFAULT_KEYMAP_C = """#include QMK_KEYBOARD_H
 | 
			
		||||
__INCLUDES__
 | 
			
		||||
 | 
			
		||||
/* THIS FILE WAS GENERATED!
 | 
			
		||||
 *
 | 
			
		||||
| 
						 | 
				
			
			@ -27,6 +28,7 @@ DEFAULT_KEYMAP_C = """#include QMK_KEYBOARD_H
 | 
			
		|||
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
 | 
			
		||||
__KEYMAP_GOES_HERE__
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -149,8 +151,8 @@ def is_keymap_dir(keymap, c=True, json=True, additional_files=None):
 | 
			
		|||
    for file in files:
 | 
			
		||||
        if (keymap / file).is_file():
 | 
			
		||||
            if additional_files:
 | 
			
		||||
                for file in additional_files:
 | 
			
		||||
                    if not (keymap / file).is_file():
 | 
			
		||||
                for additional_file in additional_files:
 | 
			
		||||
                    if not (keymap / additional_file).is_file():
 | 
			
		||||
                        return False
 | 
			
		||||
 | 
			
		||||
            return True
 | 
			
		||||
| 
						 | 
				
			
			@ -180,10 +182,11 @@ def generate_json(keymap, keyboard, layout, layers):
 | 
			
		|||
    return new_keymap
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def generate_c(keyboard, layout, layers):
 | 
			
		||||
    """Returns a `keymap.c` or `keymap.json` for the specified keyboard, layout, and layers.
 | 
			
		||||
def generate_c(keymap_json):
 | 
			
		||||
    """Returns a `keymap.c`.
 | 
			
		||||
 | 
			
		||||
    `keymap_json` is a dictionary with the following keys:
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        keyboard
 | 
			
		||||
            The name of the keyboard
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -192,19 +195,89 @@ def generate_c(keyboard, layout, layers):
 | 
			
		|||
 | 
			
		||||
        layers
 | 
			
		||||
            An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode.
 | 
			
		||||
 | 
			
		||||
        macros
 | 
			
		||||
            A sequence of strings containing macros to implement for this keyboard.
 | 
			
		||||
    """
 | 
			
		||||
    new_keymap = template_c(keyboard)
 | 
			
		||||
    new_keymap = template_c(keymap_json['keyboard'])
 | 
			
		||||
    layer_txt = []
 | 
			
		||||
    for layer_num, layer in enumerate(layers):
 | 
			
		||||
 | 
			
		||||
    for layer_num, layer in enumerate(keymap_json['layers']):
 | 
			
		||||
        if layer_num != 0:
 | 
			
		||||
            layer_txt[-1] = layer_txt[-1] + ','
 | 
			
		||||
        layer = map(_strip_any, layer)
 | 
			
		||||
        layer_keys = ', '.join(layer)
 | 
			
		||||
        layer_txt.append('\t[%s] = %s(%s)' % (layer_num, layout, layer_keys))
 | 
			
		||||
        layer_txt.append('\t[%s] = %s(%s)' % (layer_num, keymap_json['layout'], layer_keys))
 | 
			
		||||
 | 
			
		||||
    keymap = '\n'.join(layer_txt)
 | 
			
		||||
    new_keymap = new_keymap.replace('__KEYMAP_GOES_HERE__', keymap)
 | 
			
		||||
 | 
			
		||||
    if keymap_json.get('macros'):
 | 
			
		||||
        macro_txt = [
 | 
			
		||||
            'bool process_record_user(uint16_t keycode, keyrecord_t *record) {',
 | 
			
		||||
            '    if (record->event.pressed) {',
 | 
			
		||||
            '        switch (keycode) {',
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        for i, macro_array in enumerate(keymap_json['macros']):
 | 
			
		||||
            macro = []
 | 
			
		||||
 | 
			
		||||
            for macro_fragment in macro_array:
 | 
			
		||||
                if isinstance(macro_fragment, str):
 | 
			
		||||
                    macro_fragment = macro_fragment.replace('\\', '\\\\')
 | 
			
		||||
                    macro_fragment = macro_fragment.replace('\r\n', r'\n')
 | 
			
		||||
                    macro_fragment = macro_fragment.replace('\n', r'\n')
 | 
			
		||||
                    macro_fragment = macro_fragment.replace('\r', r'\n')
 | 
			
		||||
                    macro_fragment = macro_fragment.replace('\t', r'\t')
 | 
			
		||||
                    macro_fragment = macro_fragment.replace('"', r'\"')
 | 
			
		||||
 | 
			
		||||
                    macro.append(f'"{macro_fragment}"')
 | 
			
		||||
 | 
			
		||||
                elif isinstance(macro_fragment, dict):
 | 
			
		||||
                    newstring = []
 | 
			
		||||
 | 
			
		||||
                    if macro_fragment['action'] == 'delay':
 | 
			
		||||
                        newstring.append(f"SS_DELAY({macro_fragment['duration']})")
 | 
			
		||||
 | 
			
		||||
                    elif macro_fragment['action'] == 'beep':
 | 
			
		||||
                        newstring.append(r'"\a"')
 | 
			
		||||
 | 
			
		||||
                    elif macro_fragment['action'] == 'tap' and len(macro_fragment['keycodes']) > 1:
 | 
			
		||||
                        last_keycode = macro_fragment['keycodes'].pop()
 | 
			
		||||
 | 
			
		||||
                        for keycode in macro_fragment['keycodes']:
 | 
			
		||||
                            newstring.append(f'SS_DOWN(X_{keycode})')
 | 
			
		||||
 | 
			
		||||
                        newstring.append(f'SS_TAP(X_{last_keycode})')
 | 
			
		||||
 | 
			
		||||
                        for keycode in reversed(macro_fragment['keycodes']):
 | 
			
		||||
                            newstring.append(f'SS_UP(X_{keycode})')
 | 
			
		||||
 | 
			
		||||
                    else:
 | 
			
		||||
                        for keycode in macro_fragment['keycodes']:
 | 
			
		||||
                            newstring.append(f"SS_{macro_fragment['action'].upper()}(X_{keycode})")
 | 
			
		||||
 | 
			
		||||
                    macro.append(''.join(newstring))
 | 
			
		||||
 | 
			
		||||
            new_macro = "".join(macro)
 | 
			
		||||
            new_macro = new_macro.replace('""', '')
 | 
			
		||||
            macro_txt.append(f'            case MACRO_{i}:')
 | 
			
		||||
            macro_txt.append(f'                SEND_STRING({new_macro});')
 | 
			
		||||
            macro_txt.append('                return false;')
 | 
			
		||||
 | 
			
		||||
        macro_txt.append('        }')
 | 
			
		||||
        macro_txt.append('    }')
 | 
			
		||||
        macro_txt.append('\n    return true;')
 | 
			
		||||
        macro_txt.append('};')
 | 
			
		||||
        macro_txt.append('')
 | 
			
		||||
 | 
			
		||||
        new_keymap = '\n'.join((new_keymap, *macro_txt))
 | 
			
		||||
 | 
			
		||||
    if keymap_json.get('host_language'):
 | 
			
		||||
        new_keymap = new_keymap.replace('__INCLUDES__', f'#include "keymap_{keymap_json["host_language"]}.h"\n#include "sendstring_{keymap_json["host_language"]}.h"\n')
 | 
			
		||||
    else:
 | 
			
		||||
        new_keymap = new_keymap.replace('__INCLUDES__', '')
 | 
			
		||||
 | 
			
		||||
    return new_keymap
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -217,7 +290,7 @@ def write_file(keymap_filename, keymap_content):
 | 
			
		|||
    return keymap_filename
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def write_json(keyboard, keymap, layout, layers):
 | 
			
		||||
def write_json(keyboard, keymap, layout, layers, macros=None):
 | 
			
		||||
    """Generate the `keymap.json` and write it to disk.
 | 
			
		||||
 | 
			
		||||
    Returns the filename written to.
 | 
			
		||||
| 
						 | 
				
			
			@ -235,19 +308,19 @@ def write_json(keyboard, keymap, layout, layers):
 | 
			
		|||
        layers
 | 
			
		||||
            An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode.
 | 
			
		||||
    """
 | 
			
		||||
    keymap_json = generate_json(keyboard, keymap, layout, layers)
 | 
			
		||||
    keymap_json = generate_json(keyboard, keymap, layout, layers, macros=None)
 | 
			
		||||
    keymap_content = json.dumps(keymap_json)
 | 
			
		||||
    keymap_file = qmk.path.keymap(keyboard) / keymap / 'keymap.json'
 | 
			
		||||
 | 
			
		||||
    return write_file(keymap_file, keymap_content)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def write(keyboard, keymap, layout, layers):
 | 
			
		||||
def write(keymap_json):
 | 
			
		||||
    """Generate the `keymap.c` and write it to disk.
 | 
			
		||||
 | 
			
		||||
    Returns the filename written to.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
    `keymap_json` should be a dict with the following keys:
 | 
			
		||||
        keyboard
 | 
			
		||||
            The name of the keyboard
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -259,9 +332,12 @@ def write(keyboard, keymap, layout, layers):
 | 
			
		|||
 | 
			
		||||
        layers
 | 
			
		||||
            An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode.
 | 
			
		||||
 | 
			
		||||
        macros
 | 
			
		||||
            A list of macros for this keymap.
 | 
			
		||||
    """
 | 
			
		||||
    keymap_content = generate_c(keyboard, layout, layers)
 | 
			
		||||
    keymap_file = qmk.path.keymap(keyboard) / keymap / 'keymap.c'
 | 
			
		||||
    keymap_content = generate_c(keymap_json)
 | 
			
		||||
    keymap_file = qmk.path.keymap(keymap_json['keyboard']) / keymap_json['keymap'] / 'keymap.c'
 | 
			
		||||
 | 
			
		||||
    return write_file(keymap_file, keymap_content)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@
 | 
			
		|||
    "layouts": {
 | 
			
		||||
        "LAYOUT": {
 | 
			
		||||
            "layout": [
 | 
			
		||||
                { "label": "KC_A", "x": 0, "y": 0, "matrix": [0, 0] }
 | 
			
		||||
                { "label": "KC_A", "matrix": [0, 0], "x": 0, "y": 0 }
 | 
			
		||||
            ]
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -81,9 +81,9 @@ def test_hello():
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
def test_format_python():
 | 
			
		||||
    result = check_subcommand('format-python', '--dry-run')
 | 
			
		||||
    result = check_subcommand('format-python', '-n', '-a')
 | 
			
		||||
    check_returncode(result)
 | 
			
		||||
    assert 'Python code in `lib/python` is correctly formatted.' in result.stdout
 | 
			
		||||
    assert 'Successfully formatted the python code.' in result.stdout
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_list_keyboards():
 | 
			
		||||
| 
						 | 
				
			
			@ -142,6 +142,14 @@ def test_json2c():
 | 
			
		|||
    assert result.stdout == '#include QMK_KEYBOARD_H\nconst uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {\t[0] = LAYOUT_ortho_1x1(KC_A)};\n\n'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_json2c_macros():
 | 
			
		||||
    result = check_subcommand("json2c", 'keyboards/handwired/pytest/macro/keymaps/default/keymap.json')
 | 
			
		||||
    check_returncode(result)
 | 
			
		||||
    assert 'LAYOUT_ortho_1x1(MACRO_0)' in result.stdout
 | 
			
		||||
    assert 'case MACRO_0:' in result.stdout
 | 
			
		||||
    assert 'SEND_STRING("Hello, World!"SS_TAP(X_ENTER));' in result.stdout
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_json2c_stdin():
 | 
			
		||||
    result = check_subcommand_stdin('keyboards/handwired/pytest/has_template/keymaps/default_json/keymap.json', 'json2c', '-')
 | 
			
		||||
    check_returncode(result)
 | 
			
		||||
| 
						 | 
				
			
			@ -151,7 +159,7 @@ def test_json2c_stdin():
 | 
			
		|||
def test_info():
 | 
			
		||||
    result = check_subcommand('info', '-kb', 'handwired/pytest/basic')
 | 
			
		||||
    check_returncode(result)
 | 
			
		||||
    assert 'Keyboard Name: handwired/pytest/basic' in result.stdout
 | 
			
		||||
    assert 'Keyboard Name: pytest' in result.stdout
 | 
			
		||||
    assert 'Processor: atmega32u4' in result.stdout
 | 
			
		||||
    assert 'Layout:' not in result.stdout
 | 
			
		||||
    assert 'k0' not in result.stdout
 | 
			
		||||
| 
						 | 
				
			
			@ -160,7 +168,7 @@ def test_info():
 | 
			
		|||
def test_info_keyboard_render():
 | 
			
		||||
    result = check_subcommand('info', '-kb', 'handwired/pytest/basic', '-l')
 | 
			
		||||
    check_returncode(result)
 | 
			
		||||
    assert 'Keyboard Name: handwired/pytest/basic' in result.stdout
 | 
			
		||||
    assert 'Keyboard Name: pytest' in result.stdout
 | 
			
		||||
    assert 'Processor: atmega32u4' in result.stdout
 | 
			
		||||
    assert 'Layouts:' in result.stdout
 | 
			
		||||
    assert 'k0' in result.stdout
 | 
			
		||||
| 
						 | 
				
			
			@ -169,7 +177,7 @@ def test_info_keyboard_render():
 | 
			
		|||
def test_info_keymap_render():
 | 
			
		||||
    result = check_subcommand('info', '-kb', 'handwired/pytest/basic', '-km', 'default_json')
 | 
			
		||||
    check_returncode(result)
 | 
			
		||||
    assert 'Keyboard Name: handwired/pytest/basic' in result.stdout
 | 
			
		||||
    assert 'Keyboard Name: pytest' in result.stdout
 | 
			
		||||
    assert 'Processor: atmega32u4' in result.stdout
 | 
			
		||||
 | 
			
		||||
    if is_windows:
 | 
			
		||||
| 
						 | 
				
			
			@ -181,7 +189,7 @@ def test_info_keymap_render():
 | 
			
		|||
def test_info_matrix_render():
 | 
			
		||||
    result = check_subcommand('info', '-kb', 'handwired/pytest/basic', '-m')
 | 
			
		||||
    check_returncode(result)
 | 
			
		||||
    assert 'Keyboard Name: handwired/pytest/basic' in result.stdout
 | 
			
		||||
    assert 'Keyboard Name: pytest' in result.stdout
 | 
			
		||||
    assert 'Processor: atmega32u4' in result.stdout
 | 
			
		||||
    assert 'LAYOUT_ortho_1x1' in result.stdout
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -242,7 +250,7 @@ def test_generate_config_h():
 | 
			
		|||
    assert '#   define DESCRIPTION handwired/pytest/basic' in result.stdout
 | 
			
		||||
    assert '#   define DIODE_DIRECTION COL2ROW' in result.stdout
 | 
			
		||||
    assert '#   define MANUFACTURER none' in result.stdout
 | 
			
		||||
    assert '#   define PRODUCT handwired/pytest/basic' in result.stdout
 | 
			
		||||
    assert '#   define PRODUCT pytest' in result.stdout
 | 
			
		||||
    assert '#   define PRODUCT_ID 0x6465' in result.stdout
 | 
			
		||||
    assert '#   define VENDOR_ID 0xFEED' in result.stdout
 | 
			
		||||
    assert '#   define MATRIX_COLS 1' in result.stdout
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,7 +22,13 @@ def test_template_json_pytest_has_template():
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
def test_generate_c_pytest_has_template():
 | 
			
		||||
    templ = qmk.keymap.generate_c('handwired/pytest/has_template', 'LAYOUT', [['KC_A']])
 | 
			
		||||
    keymap_json = {
 | 
			
		||||
        'keyboard': 'handwired/pytest/has_template',
 | 
			
		||||
        'layout': 'LAYOUT',
 | 
			
		||||
        'layers': [['KC_A']],
 | 
			
		||||
        'macros': None,
 | 
			
		||||
    }
 | 
			
		||||
    templ = qmk.keymap.generate_c(keymap_json)
 | 
			
		||||
    assert templ == '#include QMK_KEYBOARD_H\nconst uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {\t[0] = LAYOUT(KC_A)};\n'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										1
									
								
								lib/ugfx
									
										
									
									
									
								
							
							
						
						
									
										1
									
								
								lib/ugfx
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1 +0,0 @@
 | 
			
		|||
Subproject commit 40b48f470addad6a4fb1177de1a69a181158739b
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue