Merge remote-tracking branch 'upstream/develop' into xap

This commit is contained in:
Nick Brassel 2021-11-28 12:56:26 +11:00
commit bf66b91433
5591 changed files with 131128 additions and 54530 deletions

View file

@ -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
View 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.")

View file

@ -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()

View file

@ -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()

View file

@ -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.

View file

@ -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')

View file

@ -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)

View file

@ -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)

View 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

View file

@ -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)

View 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)

View file

@ -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:

View file

@ -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")')

View file

@ -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']:

View file

@ -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':

View file

@ -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)

View file

@ -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)

View 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)

View file

@ -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)

View file

@ -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'

View file

@ -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'

View file

@ -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)

View file

@ -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)

View file

@ -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 }
]
}
}

View file

@ -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

View file

@ -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'