Remerge 0.16.x' into firmware21

This commit is contained in:
Drashna Jael're
2022-03-25 16:19:22 -07:00
548 changed files with 14528 additions and 7820 deletions

View File

@@ -69,6 +69,7 @@ subcommands = [
'qmk.cli.new.keymap',
'qmk.cli.pyformat',
'qmk.cli.pytest',
'qmk.cli.via2json',
]

View File

@@ -2,6 +2,7 @@
"""
import sys
import os
import subprocess
from milc import cli
@@ -41,6 +42,6 @@ def cd(cli):
# 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)
subprocess.run([os.environ.get('SHELL', '/usr/bin/bash')], env=qmk_env)
else:
cli.log.info("Already within qmk_firmware directory.")

View File

@@ -7,7 +7,6 @@ from subprocess import DEVNULL
from milc import cli
from qmk import submodules
from qmk.constants import QMK_FIRMWARE
class CheckStatus(Enum):
@@ -150,14 +149,3 @@ def is_executable(command):
cli.log.error("{fg_red}Can't run `%s %s`", command, version_arg)
return False
def check_git_repo():
"""Checks that the .git directory exists inside QMK_HOME.
This is a decent enough indicator that the qmk_firmware directory is a
proper Git repository, rather than a .zip download from GitHub.
"""
dot_git = QMK_FIRMWARE / '.git'
return CheckStatus.OK if dot_git.exists() else CheckStatus.WARNING

View File

@@ -11,7 +11,8 @@ from milc.questions import yesno
from qmk import submodules
from qmk.constants import QMK_FIRMWARE, QMK_FIRMWARE_UPSTREAM
from .check import CheckStatus, check_binaries, check_binary_versions, check_submodules
from qmk.commands import git_check_repo, git_get_branch, git_is_dirty, git_get_remotes, git_check_deviation, in_virtualenv
from qmk.git import git_check_repo, git_get_branch, git_get_tag, git_is_dirty, git_get_remotes, git_check_deviation
from qmk.commands import in_virtualenv
def os_tests():
@@ -47,6 +48,11 @@ def git_tests():
git_branch = git_get_branch()
if git_branch:
cli.log.info('Git branch: %s', git_branch)
repo_version = git_get_tag()
if repo_version:
cli.log.info('Repo version: %s', repo_version)
git_dirty = git_is_dirty()
if git_dirty:
cli.log.warning('{fg_yellow}Git has unstashed/uncommitted changes.')

View File

@@ -9,7 +9,9 @@ from qmk.info import info_json
from qmk.json_schema import json_load, validate
from qmk.keyboard import keyboard_completer, keyboard_folder
from qmk.keymap import locate_keymap
from qmk.commands import dump_lines
from qmk.path import normpath
from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE
def direct_pins(direct_pins, postfix):
@@ -21,18 +23,7 @@ def direct_pins(direct_pins, postfix):
cols = ','.join(map(str, [col or 'NO_PIN' for col in row]))
rows.append('{' + cols + '}')
col_count = len(direct_pins[0])
row_count = len(direct_pins)
return f"""
#ifndef MATRIX_COLS{postfix}
# define MATRIX_COLS{postfix} {col_count}
#endif // MATRIX_COLS{postfix}
#ifndef MATRIX_ROWS{postfix}
# define MATRIX_ROWS{postfix} {row_count}
#endif // MATRIX_ROWS{postfix}
#ifndef DIRECT_PINS{postfix}
# define DIRECT_PINS{postfix} {{ {", ".join(rows)} }}
#endif // DIRECT_PINS{postfix}
@@ -42,14 +33,9 @@ def direct_pins(direct_pins, postfix):
def pin_array(define, pins, postfix):
"""Return the config.h lines that set a pin array.
"""
pin_num = len(pins)
pin_array = ', '.join(map(str, [pin or 'NO_PIN' for pin in pins]))
return f"""
#ifndef {define}S{postfix}
# define {define}S{postfix} {pin_num}
#endif // {define}S{postfix}
#ifndef {define}_PINS{postfix}
# define {define}_PINS{postfix} {{ {pin_array} }}
#endif // {define}_PINS{postfix}
@@ -73,6 +59,24 @@ def matrix_pins(matrix_pins, postfix=''):
return '\n'.join(pins)
def generate_matrix_size(kb_info_json, config_h_lines):
"""Add the matrix size to the config.h.
"""
if 'matrix_pins' in kb_info_json:
col_count = kb_info_json['matrix_size']['cols']
row_count = kb_info_json['matrix_size']['rows']
config_h_lines.append(f"""
#ifndef MATRIX_COLS
# define MATRIX_COLS {col_count}
#endif // MATRIX_COLS
#ifndef MATRIX_ROWS
# define MATRIX_ROWS {row_count}
#endif // MATRIX_ROWS
""")
def generate_config_items(kb_info_json, config_h_lines):
"""Iterate through the info_config map to generate basic config values.
"""
@@ -108,6 +112,12 @@ def generate_config_items(kb_info_json, config_h_lines):
config_h_lines.append(f'#ifndef {key}')
config_h_lines.append(f'# define {key} {value}')
config_h_lines.append(f'#endif // {key}')
elif key_type == 'bcd_version':
(major, minor, revision) = config_value.split('.')
config_h_lines.append('')
config_h_lines.append(f'#ifndef {config_key}')
config_h_lines.append(f'# define {config_key} 0x{major.zfill(2)}{minor}{revision}')
config_h_lines.append(f'#endif // {config_key}')
else:
config_h_lines.append('')
config_h_lines.append(f'#ifndef {config_key}')
@@ -173,10 +183,12 @@ 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 = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#pragma once']
generate_config_items(kb_info_json, config_h_lines)
generate_matrix_size(kb_info_json, config_h_lines)
if 'matrix_pins' in kb_info_json:
config_h_lines.append(matrix_pins(kb_info_json['matrix_pins']))
@@ -184,16 +196,4 @@ def generate_config_h(cli):
generate_split_config(kb_info_json, config_h_lines)
# Show the results
config_h = '\n'.join(config_h_lines)
if cli.args.output:
cli.args.output.parent.mkdir(parents=True, exist_ok=True)
if cli.args.output.exists():
cli.args.output.replace(cli.args.output.parent / (cli.args.output.name + '.bak'))
cli.args.output.write_text(config_h)
if not cli.args.quiet:
cli.log.info('Wrote info_config.h to %s.', cli.args.output)
else:
print(config_h)
dump_lines(cli.args.output, config_h_lines, cli.args.quiet)

View File

@@ -12,6 +12,15 @@ 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)
ignored_titles = ["Format code according to conventions"]
def _is_ignored(title):
for ignore in ignored_titles:
if ignore in title:
return True
return False
def _get_pr_info(cache, gh, pr_num):
pull = cache.get(f'pull:{pr_num}')
@@ -81,7 +90,9 @@ def generate_develop_pr_list(cli):
else:
normal_collection.append(info)
if "dependencies" in commit_info['pr_labels']:
if _is_ignored(commit_info['title']):
return
elif "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)
@@ -97,7 +108,7 @@ def generate_develop_pr_list(cli):
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]}
commit_info = {'hash': match.group("hash"), 'title': pr_info['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):

View File

@@ -7,6 +7,8 @@ from qmk.decorators import automagic_keyboard
from qmk.info import info_json
from qmk.path import is_keyboard, normpath
from qmk.keyboard import keyboard_completer
from qmk.commands import dump_lines
from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE
@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
@@ -30,7 +32,7 @@ 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 = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#pragma once']
keyboard_h_lines.append(f'#define MANUFACTURER {kb_info_json["manufacturer"]}')
keyboard_h_lines.append(f'#define PRODUCT {kb_info_json["keyboard_name"]} Bootloader')
@@ -45,16 +47,4 @@ def generate_dfu_header(cli):
keyboard_h_lines.append(f'#define QMK_SPEAKER {kb_info_json["qmk_lufa_bootloader.speaker"]}')
# Show the results
keyboard_h = '\n'.join(keyboard_h_lines)
if cli.args.output:
cli.args.output.parent.mkdir(parents=True, exist_ok=True)
if cli.args.output.exists():
cli.args.output.replace(cli.args.output.parent / (cli.args.output.name + '.bak'))
cli.args.output.write_text(keyboard_h)
if not cli.args.quiet:
cli.log.info('Wrote Keyboard.h to %s.', cli.args.output)
else:
print(keyboard_h)
dump_lines(cli.args.output, keyboard_h_lines, cli.args.quiet)

View File

@@ -7,7 +7,9 @@ from subprocess import DEVNULL
from milc import cli
DOCS_PATH = Path('docs/')
BUILD_PATH = Path('.build/docs/')
BUILD_PATH = Path('.build/')
BUILD_DOCS_PATH = BUILD_PATH / 'docs'
DOXYGEN_PATH = BUILD_PATH / 'doxygen'
@cli.subcommand('Build QMK documentation.', hidden=False if cli.config.user.developer else True)
@@ -18,10 +20,12 @@ def generate_docs(cli):
* [ ] Add a real build step... something static docs
"""
if BUILD_PATH.exists():
shutil.rmtree(BUILD_PATH)
if BUILD_DOCS_PATH.exists():
shutil.rmtree(BUILD_DOCS_PATH)
if DOXYGEN_PATH.exists():
shutil.rmtree(DOXYGEN_PATH)
shutil.copytree(DOCS_PATH, BUILD_PATH)
shutil.copytree(DOCS_PATH, BUILD_DOCS_PATH)
# When not verbose we want to hide all output
args = {
@@ -34,6 +38,6 @@ def generate_docs(cli):
# Generate internal docs
cli.run(['doxygen', 'Doxyfile'], **args)
cli.run(['moxygen', '-q', '-a', '-g', '-o', BUILD_PATH / 'internals_%s.md', 'doxygen/xml'], **args)
cli.run(['moxygen', '-q', '-g', '-o', BUILD_DOCS_PATH / 'internals_%s.md', DOXYGEN_PATH / 'xml'], **args)
cli.log.info('Successfully generated internal docs to %s.', BUILD_PATH)
cli.log.info('Successfully generated internal docs to %s.', BUILD_DOCS_PATH)

View File

@@ -3,8 +3,10 @@
from milc import cli
from qmk.info import info_json
from qmk.commands import dump_lines
from qmk.keyboard import keyboard_completer, keyboard_folder
from qmk.path import normpath
from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE
def would_populate_layout_h(keyboard):
@@ -36,22 +38,10 @@ 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 = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#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")')
# Show the results
keyboard_h = '\n'.join(keyboard_h_lines) + '\n'
if cli.args.output:
cli.args.output.parent.mkdir(parents=True, exist_ok=True)
if cli.args.output.exists():
cli.args.output.replace(cli.args.output.parent / (cli.args.output.name + '.bak'))
cli.args.output.write_text(keyboard_h)
if not cli.args.quiet:
cli.log.info('Wrote keyboard_h to %s.', cli.args.output)
else:
print(keyboard_h)
dump_lines(cli.args.output, keyboard_h_lines, cli.args.quiet)

View File

@@ -2,11 +2,12 @@
"""
from milc import cli
from qmk.constants import COL_LETTERS, ROW_LETTERS
from qmk.constants import COL_LETTERS, ROW_LETTERS, GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE
from qmk.decorators import automagic_keyboard, automagic_keymap
from qmk.info import info_json
from qmk.keyboard import keyboard_completer, keyboard_folder
from qmk.path import is_keyboard, normpath
from qmk.commands import dump_lines
usb_properties = {
'vid': 'VENDOR_ID',
@@ -38,18 +39,14 @@ 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 = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#pragma once']
if 'matrix_pins' in kb_info_json:
if 'direct' in kb_info_json['matrix_pins']:
col_num = len(kb_info_json['matrix_pins']['direct'][0])
row_num = len(kb_info_json['matrix_pins']['direct'])
elif 'cols' in kb_info_json['matrix_pins'] and 'rows' in kb_info_json['matrix_pins']:
col_num = len(kb_info_json['matrix_pins']['cols'])
row_num = len(kb_info_json['matrix_pins']['rows'])
else:
cli.log.error('%s: Invalid matrix config.', cli.config.generate_layouts.keyboard)
return False
if 'matrix_size' not in kb_info_json:
cli.log.error('%s: Invalid matrix config.', cli.config.generate_layouts.keyboard)
return False
col_num = kb_info_json['matrix_size']['cols']
row_num = kb_info_json['matrix_size']['rows']
for layout_name in kb_info_json['layouts']:
if kb_info_json['layouts'][layout_name]['c_macro']:
@@ -90,16 +87,4 @@ def generate_layouts(cli):
layouts_h_lines.append('#endif')
# Show the results
layouts_h = '\n'.join(layouts_h_lines) + '\n'
if cli.args.output:
cli.args.output.parent.mkdir(parents=True, exist_ok=True)
if cli.args.output.exists():
cli.args.output.replace(cli.args.output.parent / (cli.args.output.name + '.bak'))
cli.args.output.write_text(layouts_h)
if not cli.args.quiet:
cli.log.info('Wrote info_config.h to %s.', cli.args.output)
else:
print(layouts_h)
dump_lines(cli.args.output, layouts_h_lines, cli.args.quiet)

View File

@@ -9,7 +9,9 @@ from qmk.info import info_json
from qmk.json_schema import json_load, validate
from qmk.keyboard import keyboard_completer, keyboard_folder
from qmk.keymap import locate_keymap
from qmk.commands import dump_lines
from qmk.path import normpath
from qmk.constants import GPL2_HEADER_SH_LIKE, GENERATED_HEADER_SH_LIKE
def process_mapping_rule(kb_info_json, rules_key, info_dict):
@@ -29,7 +31,7 @@ def process_mapping_rule(kb_info_json, rules_key, info_dict):
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"}'
return f'{rules_key} ?= {"yes" if rules_value else "no"}'
elif key_type == 'mapping':
return '\n'.join([f'{key} ?= {value}' for key, value in rules_value.items()])
@@ -55,7 +57,7 @@ def generate_rules_mk(cli):
kb_info_json = dotty(info_json(cli.args.keyboard))
info_rules_map = json_load(Path('data/mappings/info_rules.json'))
rules_mk_lines = ['# This file was generated by `qmk generate-rules-mk`. Do not edit or copy.', '']
rules_mk_lines = [GPL2_HEADER_SH_LIKE, GENERATED_HEADER_SH_LIKE]
# Iterate through the info_rules map to generate basic rules
for rules_key, info_dict in info_rules_map.items():
@@ -83,14 +85,9 @@ def generate_rules_mk(cli):
rules_mk_lines.append('CUSTOM_MATRIX ?= yes')
# Show the results
rules_mk = '\n'.join(rules_mk_lines) + '\n'
dump_lines(cli.args.output, rules_mk_lines)
if cli.args.output:
cli.args.output.parent.mkdir(parents=True, exist_ok=True)
if cli.args.output.exists():
cli.args.output.replace(cli.args.output.parent / (cli.args.output.name + '.bak'))
cli.args.output.write_text(rules_mk)
if cli.args.quiet:
if cli.args.escape:
print(cli.args.output.as_posix().replace(' ', '\\ '))
@@ -98,6 +95,3 @@ def generate_rules_mk(cli):
print(cli.args.output)
else:
cli.log.info('Wrote rules.mk to %s.', cli.args.output)
else:
print(rules_mk)

View File

@@ -1,9 +1,15 @@
"""Used by the make system to generate version.h for use in code.
"""
from time import strftime
from milc import cli
from qmk.commands import create_version_h
from qmk.path import normpath
from qmk.commands import dump_lines
from qmk.git import git_get_version
from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE
TIME_FMT = '%Y-%m-%d-%H:%M:%S'
@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
@@ -17,12 +23,29 @@ def generate_version_h(cli):
if cli.args.skip_all:
cli.args.skip_git = True
version_h = create_version_h(cli.args.skip_git, cli.args.skip_all)
if cli.args.output:
cli.args.output.write_text(version_h)
if not cli.args.quiet:
cli.log.info('Wrote version.h to %s.', cli.args.output)
if cli.args.skip_all:
current_time = "1970-01-01-00:00:00"
else:
print(version_h)
current_time = strftime(TIME_FMT)
if cli.args.skip_git:
git_version = "NA"
chibios_version = "NA"
chibios_contrib_version = "NA"
else:
git_version = git_get_version() or current_time
chibios_version = git_get_version("chibios", "os") or current_time
chibios_contrib_version = git_get_version("chibios-contrib", "os") or current_time
# Build the version.h file.
version_h_lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#pragma once']
version_h_lines.append(f"""
#define QMK_VERSION "{git_version}"
#define QMK_BUILDDATE "{current_time}"
#define CHIBIOS_VERSION "{chibios_version}"
#define CHIBIOS_CONTRIB_VERSION "{chibios_contrib_version}"
""")
# Show the results
dump_lines(cli.args.output, version_h_lines, cli.args.quiet)

View File

@@ -1,12 +1,11 @@
"""Generate a keymap.c from a configurator export.
"""
import json
from argcomplete.completers import FilesCompleter
from milc import cli
import qmk.keymap
import qmk.path
from qmk.commands import parse_configurator_json
@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to')
@@ -19,14 +18,8 @@ def json2c(cli):
This command uses the `qmk.keymap` module to generate a keymap.c from a configurator export. The generated keymap is written to stdout, or to a file if -o is provided.
"""
try:
# Parse the configurator from json file (or stdin)
user_keymap = json.load(cli.args.filename)
except json.decoder.JSONDecodeError as ex:
cli.log.error('The JSON input does not appear to be valid.')
cli.log.error(ex)
return False
# Parse the configurator from json file (or stdin)
user_keymap = parse_configurator_json(cli.args.filename)
# Environment processing
if cli.args.output and cli.args.output.name == '-':

View File

@@ -32,6 +32,7 @@ def _is_split(keyboard_name):
@cli.argument('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.")
@cli.argument('-f', '--filter', arg_only=True, action='append', default=[], help="Filter the list of keyboards based on the supplied value in rules.mk. Supported format is 'SPLIT_KEYBOARD=yes'. May be passed multiple times.")
@cli.argument('-km', '--keymap', type=str, default='default', help="The keymap name to build. Default is 'default'.")
@cli.argument('-e', '--env', arg_only=True, action='append', default=[], help="Set a variable to be passed to make. May be passed multiple times.")
@cli.subcommand('Compile QMK Firmware for all keyboards.', hidden=False if cli.config.user.developer else True)
def multibuild(cli):
"""Compile QMK Firmware against all keyboards.
@@ -68,7 +69,7 @@ def multibuild(cli):
all: {keyboard_safe}_binary
{keyboard_safe}_binary:
@rm -f "{QMK_FIRMWARE}/.build/failed.log.{keyboard_safe}" || true
+@$(MAKE) -C "{QMK_FIRMWARE}" -f "{QMK_FIRMWARE}/build_keyboard.mk" KEYBOARD="{keyboard_name}" KEYMAP="{cli.args.keymap}" REQUIRE_PLATFORM_KEY= COLOR=true SILENT=false \\
+@$(MAKE) -C "{QMK_FIRMWARE}" -f "{QMK_FIRMWARE}/builddefs/build_keyboard.mk" KEYBOARD="{keyboard_name}" KEYMAP="{cli.args.keymap}" REQUIRE_PLATFORM_KEY= COLOR=true SILENT=false {' '.join(cli.args.env)} \\
>>"{QMK_FIRMWARE}/.build/build.log.{os.getpid()}.{keyboard_safe}" 2>&1 \\
|| cp "{QMK_FIRMWARE}/.build/build.log.{os.getpid()}.{keyboard_safe}" "{QMK_FIRMWARE}/.build/failed.log.{os.getpid()}.{keyboard_safe}"
@{{ grep '\[ERRORS\]' "{QMK_FIRMWARE}/.build/build.log.{os.getpid()}.{keyboard_safe}" >/dev/null 2>&1 && printf "Build %-64s \e[1;31m[ERRORS]\e[0m\\n" "{keyboard_name}:{cli.args.keymap}" ; }} \\

View File

@@ -1,15 +1,45 @@
"""This script automates the creation of new keyboard directories using a starter template.
"""
import re
import json
import shutil
from datetime import date
from pathlib import Path
import re
from dotty_dict import dotty
from qmk.commands import git_get_username
import qmk.path
from milc import cli
from milc.questions import choice, question
KEYBOARD_TYPES = ['avr', 'ps2avrgb']
from qmk.git import git_get_username
from qmk.json_schema import load_jsonschema
from qmk.path import keyboard
from qmk.json_encoders import InfoJSONEncoder
from qmk.json_schema import deep_update
from qmk.constants import MCU2BOOTLOADER
COMMUNITY = Path('layouts/default/')
TEMPLATE = Path('data/templates/keyboard/')
# defaults
schema = dotty(load_jsonschema('keyboard'))
mcu_types = sorted(schema["properties.processor.enum"], key=str.casefold)
available_layouts = sorted([x.name for x in COMMUNITY.iterdir() if x.is_dir()])
def mcu_type(mcu):
"""Callable for argparse validation.
"""
if mcu not in mcu_types:
raise ValueError
return mcu
def layout_type(layout):
"""Callable for argparse validation.
"""
if layout not in available_layouts:
raise ValueError
return layout
def keyboard_name(name):
@@ -27,9 +57,134 @@ def validate_keyboard_name(name):
return bool(regex.match(name))
def select_default_bootloader(mcu):
"""Provide sane defaults for bootloader
"""
return MCU2BOOTLOADER.get(mcu, "custom")
def replace_placeholders(src, dest, tokens):
"""Replaces the given placeholders in each template file.
"""
content = src.read_text()
for key, value in tokens.items():
content = content.replace(f'%{key}%', value)
dest.write_text(content)
def augment_community_info(src, dest):
"""Splice in any additional data into info.json
"""
info = json.loads(src.read_text())
template = json.loads(dest.read_text())
# merge community with template
deep_update(info, template)
# avoid assumptions on macro name by using the first available
first_layout = next(iter(info["layouts"].values()))["layout"]
# guess at width and height now its optional
width, height = (0, 0)
for item in first_layout:
width = max(width, int(item["x"]) + 1)
height = max(height, int(item["y"]) + 1)
info["matrix_pins"] = {
"cols": ["C2"] * width,
"rows": ["D1"] * height,
}
# assume a 1:1 mapping on matrix to electrical
for item in first_layout:
item["matrix"] = [int(item["y"]), int(item["x"])]
# finally write out the updated info.json
dest.write_text(json.dumps(info, cls=InfoJSONEncoder))
def _question(*args, **kwargs):
"""Ugly workaround until 'milc' learns to display a repromt msg
"""
# TODO: Remove this once milc.questions.question handles reprompt messages
reprompt = kwargs["reprompt"]
del kwargs["reprompt"]
validate = kwargs["validate"]
del kwargs["validate"]
prompt = args[0]
ret = None
while not ret:
ret = question(prompt, **kwargs)
if not validate(ret):
ret = None
prompt = reprompt
return ret
def prompt_keyboard():
prompt = """{fg_yellow}Name Your Keyboard Project{style_reset_all}
For more infomation, see:
https://docs.qmk.fm/#/hardware_keyboard_guidelines?id=naming-your-keyboardproject
Keyboard Name? """
errmsg = 'Keyboard already exists! Please choose a different name:'
return _question(prompt, reprompt=errmsg, validate=lambda x: not keyboard(x).exists())
def prompt_user():
prompt = """
{fg_yellow}Attribution{style_reset_all}
Used for maintainer, copyright, etc
Your GitHub Username? """
return question(prompt, default=git_get_username())
def prompt_name(def_name):
prompt = """
{fg_yellow}More Attribution{style_reset_all}
Used for maintainer, copyright, etc
Your Real Name? """
return question(prompt, default=def_name)
def prompt_layout():
prompt = """
{fg_yellow}Pick Base Layout{style_reset_all}
As a starting point, one of the common layouts can be used to bootstrap the process
Default Layout? """
# avoid overwhelming user - remove some?
filtered_layouts = [x for x in available_layouts if not any(xs in x for xs in ['_split', '_blocker', '_tsangan', '_f13'])]
filtered_layouts.append("none of the above")
return choice(prompt, filtered_layouts, default=len(filtered_layouts) - 1)
def prompt_mcu():
prompt = """
{fg_yellow}What Powers Your Project{style_reset_all}
For more infomation, see:
https://docs.qmk.fm/#/compatible_microcontrollers
MCU? """
# remove any options strictly used for compatibility
filtered_mcu = [x for x in mcu_types if not any(xs in x for xs in ['cortex', 'unknown'])]
return choice(prompt, filtered_mcu, default=filtered_mcu.index("atmega32u4"))
@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('-l', '--layout', help='Community layout to bootstrap with', arg_only=True, type=layout_type)
@cli.argument('-t', '--type', help='Specify the keyboard MCU type', arg_only=True, type=mcu_type)
@cli.argument('-u', '--username', help='Specify your username (default from Git config)', dest='name')
@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):
@@ -38,102 +193,57 @@ def new_keyboard(cli):
cli.log.info('{style_bright}Generating a new QMK keyboard directory{style_normal}')
cli.echo('')
# Get keyboard name
new_keyboard_name = None
while not new_keyboard_name:
new_keyboard_name = cli.args.keyboard if cli.args.keyboard else question('Keyboard Name:')
if not validate_keyboard_name(new_keyboard_name):
cli.log.error('Keyboard names must contain only {fg_cyan}lowercase a-z{fg_reset}, {fg_cyan}0-9{fg_reset}, and {fg_cyan}_{fg_reset}! Please choose a different name.')
kb_name = cli.args.keyboard if cli.args.keyboard else prompt_keyboard()
user_name = cli.config.new_keyboard.name if cli.config.new_keyboard.name else prompt_user()
real_name = cli.args.realname or cli.config.new_keyboard.name if cli.args.realname or cli.config.new_keyboard.name else prompt_name(user_name)
default_layout = cli.args.layout if cli.args.layout else prompt_layout()
mcu = cli.args.type if cli.args.type else prompt_mcu()
bootloader = select_default_bootloader(mcu)
# Exit if passed by arg
if cli.args.keyboard:
return False
if not validate_keyboard_name(kb_name):
cli.log.error('Keyboard names must contain only {fg_cyan}lowercase a-z{fg_reset}, {fg_cyan}0-9{fg_reset}, and {fg_cyan}_{fg_reset}! Please choose a different name.')
return 1
new_keyboard_name = None
continue
if keyboard(kb_name).exists():
cli.log.error(f'Keyboard {{fg_cyan}}{kb_name}{{fg_reset}} already exists! Please choose a different name.')
return 1
keyboard_path = qmk.path.keyboard(new_keyboard_name)
if keyboard_path.exists():
cli.log.error(f'Keyboard {{fg_cyan}}{new_keyboard_name}{{fg_reset}} already exists! Please choose a different name.')
# Exit if passed by arg
if cli.args.keyboard:
return False
new_keyboard_name = None
# Get keyboard type
keyboard_type = cli.args.type if cli.args.type else choice('Keyboard Type:', KEYBOARD_TYPES, default=0)
# Get username
user_name = None
while not 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.')
# Exit if passed by arg
if cli.args.username:
return False
real_name = None
while not real_name:
real_name = question('Your real name:', default=user_name)
keyboard_basename = keyboard_path.name
replacements = {
"YEAR": str(date.today().year),
"KEYBOARD": keyboard_basename,
"USER_NAME": user_name,
"YOUR_NAME": real_name,
tokens = { # Comment here is to force multiline formatting
'YEAR': str(date.today().year),
'KEYBOARD': kb_name,
'USER_NAME': user_name,
'REAL_NAME': real_name,
'LAYOUT': default_layout,
'MCU': mcu,
'BOOTLOADER': bootloader
}
template_dir = Path('data/templates')
template_tree(template_dir / 'base', keyboard_path, replacements)
template_tree(template_dir / keyboard_type, keyboard_path, replacements)
if cli.config.general.verbose:
cli.log.info("Creating keyboard with:")
for key, value in tokens.items():
cli.echo(f" {key.ljust(10)}: {value}")
cli.echo('')
cli.log.info(f'{{fg_green}}Created a new keyboard called {{fg_cyan}}{new_keyboard_name}{{fg_green}}.{{fg_reset}}')
cli.log.info(f'To start working on things, `cd` into {{fg_cyan}}{keyboard_path}{{fg_reset}},')
# TODO: detach community layout and rename to just "LAYOUT"
if default_layout == 'none of the above':
default_layout = "ortho_4x4"
# begin with making the deepest folder in the tree
keymaps_path = keyboard(kb_name) / 'keymaps/'
keymaps_path.mkdir(parents=True)
# copy in keymap.c or keymap.json
community_keymap = Path(COMMUNITY / f'{default_layout}/default_{default_layout}/')
shutil.copytree(community_keymap, keymaps_path / 'default')
# process template files
for file in list(TEMPLATE.iterdir()):
replace_placeholders(file, keyboard(kb_name) / file.name, tokens)
# merge in infos
community_info = Path(COMMUNITY / f'{default_layout}/info.json')
augment_community_info(community_info, keyboard(kb_name) / community_info.name)
cli.log.info(f'{{fg_green}}Created a new keyboard called {{fg_cyan}}{kb_name}{{fg_green}}.{{fg_reset}}')
cli.log.info(f'To start working on things, `cd` into {{fg_cyan}}keyboards/{kb_name}{{fg_reset}},')
cli.log.info('or open the directory in your preferred text editor.')
def find_user_name():
if cli.args.username:
return cli.args.username
elif cli.config.user.name:
return cli.config.user.name
else:
return git_get_username()
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
"""
dst.mkdir(parents=True, exist_ok=True)
for child in src.iterdir():
if child.is_dir():
template_tree(child, dst / child.name, replacements=replacements)
if child.is_file():
file_name = dst / (child.name % replacements)
with file_name.open(mode='x') as dst_f:
with child.open() as src_f:
template = src_f.read()
dst_f.write(template % replacements)
cli.log.info(f"And build with {{fg_yellow}}qmk compile -kb {kb_name} -km default{{fg_reset}}.")

145
lib/python/qmk/cli/via2json.py Executable file
View File

@@ -0,0 +1,145 @@
"""Generate a keymap.c from a configurator export.
"""
import json
import re
from milc import cli
import qmk.keyboard
import qmk.path
from qmk.info import info_json
from qmk.json_encoders import KeymapJSONEncoder
from qmk.commands import parse_configurator_json, dump_lines
from qmk.keymap import generate_json, list_keymaps, locate_keymap, parse_keymap_c
def _find_via_layout_macro(keyboard):
keymap_layout = None
if 'via' in list_keymaps(keyboard):
keymap_path = locate_keymap(keyboard, 'via')
if keymap_path.suffix == '.json':
keymap_layout = parse_configurator_json(keymap_path)['layout']
else:
keymap_layout = parse_keymap_c(keymap_path)['layers'][0]['layout']
return keymap_layout
def _convert_macros(via_macros):
via_macros = list(filter(lambda f: bool(f), via_macros))
if len(via_macros) == 0:
return list()
split_regex = re.compile(r'(}\,)|(\,{)')
macros = list()
for via_macro in via_macros:
# Split VIA macro to its elements
macro = split_regex.split(via_macro)
# Remove junk elements (None, '},' and ',{')
macro = list(filter(lambda f: False if f in (None, '},', ',{') else True, macro))
macro_data = list()
for m in macro:
if '{' in m or '}' in m:
# Found keycode(s)
keycodes = m.split(',')
# Remove whitespaces and curly braces from around keycodes
keycodes = list(map(lambda s: s.strip(' {}'), keycodes))
# Remove the KC prefix
keycodes = list(map(lambda s: s.replace('KC_', ''), keycodes))
macro_data.append({"action": "tap", "keycodes": keycodes})
else:
# Found text
macro_data.append(m)
macros.append(macro_data)
return macros
def _fix_macro_keys(keymap_data):
macro_no = re.compile(r'MACRO0?([0-9]{1,2})')
for i in range(0, len(keymap_data)):
for j in range(0, len(keymap_data[i])):
kc = keymap_data[i][j]
m = macro_no.match(kc)
if m:
keymap_data[i][j] = f'MACRO_{m.group(1)}'
return keymap_data
def _via_to_keymap(via_backup, keyboard_data, keymap_layout):
# Check if passed LAYOUT is correct
layout_data = keyboard_data['layouts'].get(keymap_layout)
if not layout_data:
cli.log.error(f'LAYOUT macro {keymap_layout} is not a valid one for keyboard {cli.args.keyboard}!')
exit(1)
layout_data = layout_data['layout']
sorting_hat = list()
for index, data in enumerate(layout_data):
sorting_hat.append([index, data['matrix']])
sorting_hat.sort(key=lambda k: (k[1][0], k[1][1]))
pos = 0
for row_num in range(0, keyboard_data['matrix_size']['rows']):
for col_num in range(0, keyboard_data['matrix_size']['cols']):
if pos >= len(sorting_hat) or sorting_hat[pos][1][0] != row_num or sorting_hat[pos][1][1] != col_num:
sorting_hat.insert(pos, [None, [row_num, col_num]])
else:
sorting_hat.append([None, [row_num, col_num]])
pos += 1
keymap_data = list()
for layer in via_backup['layers']:
pos = 0
layer_data = list()
for key in layer:
if sorting_hat[pos][0] is not None:
layer_data.append([sorting_hat[pos][0], key])
pos += 1
layer_data.sort()
layer_data = [kc[1] for kc in layer_data]
keymap_data.append(layer_data)
return keymap_data
@cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to')
@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
@cli.argument('filename', type=qmk.path.FileType('r'), arg_only=True, help='VIA Backup JSON file')
@cli.argument('-kb', '--keyboard', type=qmk.keyboard.keyboard_folder, completer=qmk.keyboard.keyboard_completer, arg_only=True, required=True, help='The keyboard\'s name')
@cli.argument('-km', '--keymap', arg_only=True, default='via2json', help='The keymap\'s name')
@cli.argument('-l', '--layout', arg_only=True, help='The keymap\'s layout')
@cli.subcommand('Convert a VIA backup json to keymap.json format.')
def via2json(cli):
"""Convert a VIA backup json to keymap.json format.
This command uses the `qmk.keymap` module to generate a keymap.json from a VIA backup json. The generated keymap is written to stdout, or to a file if -o is provided.
"""
# Find appropriate layout macro
keymap_layout = cli.args.layout if cli.args.layout else _find_via_layout_macro(cli.args.keyboard)
if not keymap_layout:
cli.log.error(f"Couldn't find LAYOUT macro for keyboard {cli.args.keyboard}. Please specify it with the '-l' argument.")
exit(1)
# Load the VIA backup json
with cli.args.filename.open('r') as fd:
via_backup = json.load(fd)
# Generate keyboard metadata
keyboard_data = info_json(cli.args.keyboard)
# Get keycode array
keymap_data = _via_to_keymap(via_backup, keyboard_data, keymap_layout)
# Convert macros
macro_data = list()
if via_backup.get('macros'):
macro_data = _convert_macros(via_backup['macros'])
# Replace VIA macro keys with JSON keymap ones
keymap_data = _fix_macro_keys(keymap_data)
# Generate the keymap.json
keymap_json = generate_json(cli.args.keymap, cli.args.keyboard, keymap_layout, keymap_data, macro_data)
keymap_lines = [json.dumps(keymap_json, cls=KeymapJSONEncoder)]
dump_lines(cli.args.output, keymap_lines, cli.args.quiet)

View File

@@ -1,20 +1,16 @@
"""Helper functions for commands.
"""
import json
import os
import sys
import shutil
from pathlib import Path
from subprocess import DEVNULL
from time import strftime
from milc import cli
import jsonschema
import qmk.keymap
from qmk.constants import QMK_FIRMWARE, KEYBOARD_OUTPUT_PREFIX
from qmk.json_schema import json_load
time_fmt = '%Y-%m-%d-%H:%M:%S'
from qmk.constants import KEYBOARD_OUTPUT_PREFIX
from qmk.json_schema import json_load, validate
def _find_make():
@@ -93,31 +89,6 @@ def create_make_command(keyboard, keymap, target=None, dry_run=False, parallel=1
return create_make_target(':'.join(make_args), dry_run=dry_run, parallel=parallel, **env_vars)
def get_git_version(current_time, repo_dir='.', check_dir='.'):
"""Returns the current git version for a repo, or the current time.
"""
git_describe_cmd = ['git', 'describe', '--abbrev=6', '--dirty', '--always', '--tags']
if repo_dir != '.':
repo_dir = Path('lib') / repo_dir
if check_dir != '.':
check_dir = repo_dir / check_dir
if Path(check_dir).exists():
git_describe = cli.run(git_describe_cmd, stdin=DEVNULL, cwd=repo_dir)
if git_describe.returncode == 0:
return git_describe.stdout.strip()
else:
cli.log.warn(f'"{" ".join(git_describe_cmd)}" returned error code {git_describe.returncode}')
print(git_describe.stderr)
return current_time
return current_time
def get_make_parallel_args(parallel=1):
"""Returns the arguments for running the specified number of parallel jobs.
"""
@@ -136,37 +107,6 @@ def get_make_parallel_args(parallel=1):
return parallel_args
def create_version_h(skip_git=False, skip_all=False):
"""Generate version.h contents
"""
if skip_all:
current_time = "1970-01-01-00:00:00"
else:
current_time = strftime(time_fmt)
if skip_git:
git_version = "NA"
chibios_version = "NA"
chibios_contrib_version = "NA"
else:
git_version = get_git_version(current_time)
chibios_version = get_git_version(current_time, "chibios", "os")
chibios_contrib_version = get_git_version(current_time, "chibios-contrib", "os")
version_h_lines = f"""/* This file was automatically generated. Do not edit or copy.
*/
#pragma once
#define QMK_VERSION "{git_version}"
#define QMK_BUILDDATE "{current_time}"
#define CHIBIOS_VERSION "{chibios_version}"
#define CHIBIOS_CONTRIB_VERSION "{chibios_contrib_version}"
"""
return version_h_lines
def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_vars):
"""Convert a configurator export JSON file into a C file and then compile it.
@@ -185,6 +125,10 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va
A command to run to compile and flash the C file.
"""
# In case the user passes a keymap.json from a keymap directory directly to the CLI.
# e.g.: qmk compile - < keyboards/clueboard/california/keymaps/default/keymap.json
user_keymap["keymap"] = user_keymap.get("keymap", "default_json")
# Write the keymap.c file
keyboard_filesafe = user_keymap['keyboard'].replace('/', '_')
target = f'{keyboard_filesafe}_{user_keymap["keymap"]}'
@@ -197,9 +141,6 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va
keymap_dir.mkdir(exist_ok=True, parents=True)
keymap_c.write_text(c_text)
version_h = Path('quantum/version.h')
version_h.write_text(create_version_h())
# Return a command that can be run to make the keymap and flash if given
verbose = 'true' if cli.config.general.verbose else 'false'
color = 'true' if cli.config.general.color else 'false'
@@ -213,7 +154,7 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va
'-r',
'-R',
'-f',
'build_keyboard.mk',
'builddefs/build_keyboard.mk',
])
if bootloader:
@@ -248,8 +189,15 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va
def parse_configurator_json(configurator_file):
"""Open and parse a configurator json export
"""
# FIXME(skullydazed/anyone): Add validation here
user_keymap = json.load(configurator_file)
user_keymap = json_load(configurator_file)
# Validate against the jsonschema
try:
validate(user_keymap, 'qmk.keymap.v1')
except jsonschema.ValidationError as e:
cli.log.error(f'Invalid JSON keymap: {configurator_file} : {e.message}')
exit(1)
orig_keyboard = user_keymap['keyboard']
aliases = json_load(Path('data/mappings/keyboard_aliases.json'))
@@ -263,78 +211,26 @@ def parse_configurator_json(configurator_file):
return user_keymap
def git_get_username():
"""Retrieves user's username from Git config, if set.
"""
git_username = cli.run(['git', 'config', '--get', 'user.name'])
if git_username.returncode == 0 and git_username.stdout:
return git_username.stdout.strip()
def git_check_repo():
"""Checks that the .git directory exists inside QMK_HOME.
This is a decent enough indicator that the qmk_firmware directory is a
proper Git repository, rather than a .zip download from GitHub.
"""
dot_git_dir = QMK_FIRMWARE / '.git'
return dot_git_dir.is_dir()
def git_get_branch():
"""Returns the current branch for a repo, or None.
"""
git_branch = cli.run(['git', 'branch', '--show-current'])
if not git_branch.returncode != 0 or not git_branch.stdout:
# Workaround for Git pre-2.22
git_branch = cli.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'])
if git_branch.returncode == 0:
return git_branch.stdout.strip()
def git_is_dirty():
"""Returns 1 if repo is dirty, or 0 if clean
"""
git_diff_staged_cmd = ['git', 'diff', '--quiet']
git_diff_unstaged_cmd = [*git_diff_staged_cmd, '--cached']
unstaged = cli.run(git_diff_staged_cmd)
staged = cli.run(git_diff_unstaged_cmd)
return unstaged.returncode != 0 or staged.returncode != 0
def git_get_remotes():
"""Returns the current remotes for a repo.
"""
remotes = {}
git_remote_show_cmd = ['git', 'remote', 'show']
git_remote_get_cmd = ['git', 'remote', 'get-url']
git_remote_show = cli.run(git_remote_show_cmd)
if git_remote_show.returncode == 0:
for name in git_remote_show.stdout.splitlines():
git_remote_name = cli.run([*git_remote_get_cmd, name])
remotes[name.strip()] = {"url": git_remote_name.stdout.strip()}
return remotes
def git_check_deviation(active_branch):
"""Return True if branch has custom commits
"""
cli.run(['git', 'fetch', 'upstream', active_branch])
deviations = cli.run(['git', '--no-pager', 'log', f'upstream/{active_branch}...{active_branch}'])
return bool(deviations.returncode)
def in_virtualenv():
"""Check if running inside a virtualenv.
Based on https://stackoverflow.com/a/1883251
"""
active_prefix = getattr(sys, "base_prefix", None) or getattr(sys, "real_prefix", None) or sys.prefix
return active_prefix != sys.prefix
def dump_lines(output_file, lines, quiet=True):
"""Handle dumping to stdout or file
Creates parent folders if required
"""
generated = '\n'.join(lines) + '\n'
if output_file and output_file.name != '-':
output_file.parent.mkdir(parents=True, exist_ok=True)
if output_file.exists():
output_file.replace(output_file.parent / (output_file.name + '.bak'))
output_file.write_text(generated)
if not quiet:
cli.log.info(f'Wrote {output_file.name} to {output_file}.')
else:
print(generated)

View File

@@ -1,6 +1,7 @@
"""Information that should be available to the python library.
"""
from os import environ
from datetime import date
from pathlib import Path
# The root of the qmk_firmware tree.
@@ -13,10 +14,53 @@ QMK_FIRMWARE_UPSTREAM = 'qmk/qmk_firmware'
MAX_KEYBOARD_SUBFOLDERS = 5
# Supported processor types
<<<<<<< HEAD
CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'MK66FX1M0', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F405', 'STM32F407', 'STM32F411', 'STM32F446', 'STM32G431', 'STM32G474', 'STM32L412', 'STM32L422', 'STM32L433', 'STM32L443', 'GD32VF103', 'WB32F3G71'
=======
CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'MK66FX1M0', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F405', 'STM32F407', 'STM32F411', 'STM32F446', 'STM32G431', 'STM32G474', 'STM32L412', 'STM32L422', 'STM32L432', 'STM32L433', 'STM32L442', 'STM32L443', 'GD32VF103', 'WB32F3G71'
>>>>>>> qmk/master
LUFA_PROCESSORS = 'at90usb162', 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None
VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85'
# Bootloaders of the supported processors
MCU2BOOTLOADER = {
"MKL26Z64": "halfkay",
"MK20DX128": "halfkay",
"MK20DX256": "halfkay",
"MK66FX1M0": "halfkay",
"STM32F042": "stm32-dfu",
"STM32F072": "stm32-dfu",
"STM32F103": "stm32duino",
"STM32F303": "stm32-dfu",
"STM32F401": "stm32-dfu",
"STM32F405": "stm32-dfu",
"STM32F407": "stm32-dfu",
"STM32F411": "stm32-dfu",
"STM32F446": "stm32-dfu",
"STM32G431": "stm32-dfu",
"STM32G474": "stm32-dfu",
"STM32L412": "stm32-dfu",
"STM32L422": "stm32-dfu",
"STM32L432": "stm32-dfu",
"STM32L433": "stm32-dfu",
"STM32L442": "stm32-dfu",
"STM32L443": "stm32-dfu",
"GD32VF103": "gd32v-dfu",
"WB32F3G71": "wb32-dfu",
"atmega16u2": "atmel-dfu",
"atmega32u2": "atmel-dfu",
"atmega16u4": "atmel-dfu",
"atmega32u4": "atmel-dfu",
"at90usb162": "atmel-dfu",
"at90usb646": "atmel-dfu",
"at90usb647": "atmel-dfu",
"at90usb1286": "atmel-dfu",
"at90usb1287": "atmel-dfu",
"atmega32a": "bootloadhid",
"atmega328p": "usbasploader",
"atmega328": "usbasploader",
}
# Common format strings
DATE_FORMAT = '%Y-%m-%d'
DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S %Z'
@@ -36,3 +80,65 @@ LED_INDICATORS = {
# Constants that should match their counterparts in make
BUILD_DIR = environ.get('BUILD_DIR', '.build')
KEYBOARD_OUTPUT_PREFIX = f'{BUILD_DIR}/obj_'
# Headers for generated files
GPL2_HEADER_C_LIKE = f'''\
// Copyright {date.today().year} QMK
// SPDX-License-Identifier: GPL-2.0-or-later
'''
GPL2_HEADER_SH_LIKE = f'''\
# Copyright {date.today().year} QMK
# SPDX-License-Identifier: GPL-2.0-or-later
'''
GENERATED_HEADER_C_LIKE = '''\
/*******************************************************************************
88888888888 888 d8b .d888 d8b 888 d8b
888 888 Y8P d88P" Y8P 888 Y8P
888 888 888 888
888 88888b. 888 .d8888b 888888 888 888 .d88b. 888 .d8888b
888 888 "88b 888 88K 888 888 888 d8P Y8b 888 88K
888 888 888 888 "Y8888b. 888 888 888 88888888 888 "Y8888b.
888 888 888 888 X88 888 888 888 Y8b. 888 X88
888 888 888 888 88888P' 888 888 888 "Y8888 888 88888P'
888 888
888 888
888 888
.d88b. .d88b. 88888b. .d88b. 888d888 8888b. 888888 .d88b. .d88888
d88P"88b d8P Y8b 888 "88b d8P Y8b 888P" "88b 888 d8P Y8b d88" 888
888 888 88888888 888 888 88888888 888 .d888888 888 88888888 888 888
Y88b 888 Y8b. 888 888 Y8b. 888 888 888 Y88b. Y8b. Y88b 888
"Y88888 "Y8888 888 888 "Y8888 888 "Y888888 "Y888 "Y8888 "Y88888
888
Y8b d88P
"Y88P"
*******************************************************************************/
'''
GENERATED_HEADER_SH_LIKE = '''\
################################################################################
#
# 88888888888 888 d8b .d888 d8b 888 d8b
# 888 888 Y8P d88P" Y8P 888 Y8P
# 888 888 888 888
# 888 88888b. 888 .d8888b 888888 888 888 .d88b. 888 .d8888b
# 888 888 "88b 888 88K 888 888 888 d8P Y8b 888 88K
# 888 888 888 888 "Y8888b. 888 888 888 88888888 888 "Y8888b.
# 888 888 888 888 X88 888 888 888 Y8b. 888 X88
# 888 888 888 888 88888P' 888 888 888 "Y8888 888 88888P'
#
# 888 888
# 888 888
# 888 888
# .d88b. .d88b. 88888b. .d88b. 888d888 8888b. 888888 .d88b. .d88888
# d88P"88b d8P Y8b 888 "88b d8P Y8b 888P" "88b 888 d8P Y8b d88" 888
# 888 888 88888888 888 888 88888888 888 .d888888 888 88888888 888 888
# Y88b 888 Y8b. 888 888 Y8b. 888 888 888 Y88b. Y8b. Y88b 888
# "Y88888 "Y8888 888 888 "Y8888 888 "Y888888 "Y888 "Y8888 "Y88888
# 888
# Y8b d88P
# "Y88P"
#
################################################################################
'''

110
lib/python/qmk/git.py Normal file
View File

@@ -0,0 +1,110 @@
"""Functions for working with the QMK repo.
"""
from subprocess import DEVNULL
from pathlib import Path
from milc import cli
from qmk.constants import QMK_FIRMWARE
def git_get_version(repo_dir='.', check_dir='.'):
"""Returns the current git version for a repo, or None.
"""
git_describe_cmd = ['git', 'describe', '--abbrev=6', '--dirty', '--always', '--tags']
if repo_dir != '.':
repo_dir = Path('lib') / repo_dir
if check_dir != '.':
check_dir = repo_dir / check_dir
if Path(check_dir).exists():
git_describe = cli.run(git_describe_cmd, stdin=DEVNULL, cwd=repo_dir)
if git_describe.returncode == 0:
return git_describe.stdout.strip()
else:
cli.log.warn(f'"{" ".join(git_describe_cmd)}" returned error code {git_describe.returncode}')
print(git_describe.stderr)
return None
return None
def git_get_username():
"""Retrieves user's username from Git config, if set.
"""
git_username = cli.run(['git', 'config', '--get', 'user.name'])
if git_username.returncode == 0 and git_username.stdout:
return git_username.stdout.strip()
def git_get_branch():
"""Returns the current branch for a repo, or None.
"""
git_branch = cli.run(['git', 'branch', '--show-current'])
if not git_branch.returncode != 0 or not git_branch.stdout:
# Workaround for Git pre-2.22
git_branch = cli.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'])
if git_branch.returncode == 0:
return git_branch.stdout.strip()
def git_get_tag():
"""Returns the current tag for a repo, or None.
"""
git_tag = cli.run(['git', 'describe', '--abbrev=0', '--tags'])
if git_tag.returncode == 0:
return git_tag.stdout.strip()
def git_get_remotes():
"""Returns the current remotes for a repo.
"""
remotes = {}
git_remote_show_cmd = ['git', 'remote', 'show']
git_remote_get_cmd = ['git', 'remote', 'get-url']
git_remote_show = cli.run(git_remote_show_cmd)
if git_remote_show.returncode == 0:
for name in git_remote_show.stdout.splitlines():
git_remote_name = cli.run([*git_remote_get_cmd, name])
remotes[name.strip()] = {"url": git_remote_name.stdout.strip()}
return remotes
def git_is_dirty():
"""Returns 1 if repo is dirty, or 0 if clean
"""
git_diff_staged_cmd = ['git', 'diff', '--quiet']
git_diff_unstaged_cmd = [*git_diff_staged_cmd, '--cached']
unstaged = cli.run(git_diff_staged_cmd)
staged = cli.run(git_diff_unstaged_cmd)
return unstaged.returncode != 0 or staged.returncode != 0
def git_check_repo():
"""Checks that the .git directory exists inside QMK_HOME.
This is a decent enough indicator that the qmk_firmware directory is a
proper Git repository, rather than a .zip download from GitHub.
"""
dot_git_dir = QMK_FIRMWARE / '.git'
return dot_git_dir.is_dir()
def git_check_deviation(active_branch):
"""Return True if branch has custom commits
"""
cli.run(['git', 'fetch', 'upstream', active_branch])
deviations = cli.run(['git', '--no-pager', 'log', f'upstream/{active_branch}...{active_branch}'])
return bool(deviations.returncode)

View File

@@ -387,6 +387,19 @@ def _extract_matrix_info(info_data, config_c):
return info_data
# TODO: kill off usb.device_ver in favor of usb.device_version
def _extract_device_version(info_data):
if info_data.get('usb'):
if info_data['usb'].get('device_version') and not info_data['usb'].get('device_ver'):
(major, minor, revision) = info_data['usb']['device_version'].split('.', 3)
info_data['usb']['device_ver'] = f'0x{major.zfill(2)}{minor}{revision}'
if not info_data['usb'].get('device_version') and info_data['usb'].get('device_ver'):
major = int(info_data['usb']['device_ver'][2:4])
minor = int(info_data['usb']['device_ver'][4])
revision = int(info_data['usb']['device_ver'][5])
info_data['usb']['device_version'] = f'{major}.{minor}.{revision}'
def _extract_config_h(info_data):
"""Pull some keyboard information from existing config.h files
"""
@@ -430,6 +443,13 @@ def _extract_config_h(info_data):
elif key_type == 'int':
dotty_info[info_key] = int(config_c[config_key])
elif key_type == 'bcd_version':
major = int(config_c[config_key][2:4])
minor = int(config_c[config_key][4])
revision = int(config_c[config_key][5])
dotty_info[info_key] = f'{major}.{minor}.{revision}'
else:
dotty_info[info_key] = config_c[config_key]
@@ -444,6 +464,7 @@ def _extract_config_h(info_data):
_extract_split_main(info_data, config_c)
_extract_split_transport(info_data, config_c)
_extract_split_right_pins(info_data, config_c)
_extract_device_version(info_data)
return info_data
@@ -529,6 +550,11 @@ def _matrix_size(info_data):
info_data['matrix_size']['cols'] = len(info_data['matrix_pins']['cols'])
info_data['matrix_size']['rows'] = len(info_data['matrix_pins']['rows'])
# Assumption of split common
if 'split' in info_data:
if info_data['split'].get('enabled', False):
info_data['matrix_size']['rows'] *= 2
return info_data
@@ -622,12 +648,7 @@ def arm_processor_rules(info_data, rules):
info_data['protocol'] = 'ChibiOS'
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'
info_data['bootloader'] = 'unknown'
if 'STM32' in info_data['processor']:
info_data['platform'] = 'STM32'

View File

@@ -146,7 +146,13 @@ class KeymapJSONEncoder(QMKJSONEncoder):
if key == 'JSON_NEWLINE':
layer.append([])
else:
layer[-1].append(f'"{key}"')
if isinstance(key, dict):
# We have a macro
# TODO: Add proper support for nicely formatting keymap.json macros
layer[-1].append(f'{self.encode(key)}')
else:
layer[-1].append(f'"{key}"')
layer = [f"{self.indent_str*indent_level}{', '.join(row)}" for row in layer]

View File

@@ -16,7 +16,11 @@ def json_load(json_file):
Note: file must be a Path object.
"""
try:
return hjson.load(json_file.open(encoding='utf-8'))
# Get the IO Stream for Path objects
# Not necessary if the data is provided via stdin
if isinstance(json_file, Path):
json_file = json_file.open(encoding='utf-8')
return hjson.load(json_file)
except (json.decoder.JSONDecodeError, hjson.HjsonDecodeError) as e:
cli.log.error('Invalid JSON encountered attempting to load {fg_cyan}%s{fg_reset}:\n\t{fg_red}%s', json_file, e)
@@ -62,7 +66,7 @@ def create_validator(schema):
"""Creates a validator for the given schema id.
"""
schema_store = compile_schema_store()
resolver = jsonschema.RefResolver.from_schema(schema_store['qmk.keyboard.v1'], store=schema_store)
resolver = jsonschema.RefResolver.from_schema(schema_store[schema], store=schema_store)
return jsonschema.Draft7Validator(schema_store[schema], resolver=resolver).validate

View File

@@ -162,13 +162,12 @@ def render_layout(layout_data, render_ascii, key_labels=None):
"""
textpad = [array('u', ' ' * 200) for x in range(100)]
style = 'ascii' if render_ascii else 'unicode'
box_chars = BOX_DRAWING_CHARACTERS[style]
for key in layout_data:
x = ceil(key.get('x', 0) * 4)
y = ceil(key.get('y', 0) * 3)
w = ceil(key.get('w', 1) * 4)
h = ceil(key.get('h', 1) * 3)
x = key.get('x', 0)
y = key.get('y', 0)
w = key.get('w', 1)
h = key.get('h', 1)
if key_labels:
label = key_labels.pop(0)
@@ -177,26 +176,12 @@ def render_layout(layout_data, render_ascii, key_labels=None):
else:
label = key.get('label', '')
label_len = w - 2
label_leftover = label_len - len(label)
if len(label) > label_len:
label = label[:label_len]
label_blank = ' ' * label_len
label_border = box_chars['h'] * label_len
label_middle = label + ' '*label_leftover # noqa: yapf insists there be no whitespace around *
top_line = array('u', box_chars['tl'] + label_border + box_chars['tr'])
lab_line = array('u', box_chars['v'] + label_middle + box_chars['v'])
mid_line = array('u', box_chars['v'] + label_blank + box_chars['v'])
bot_line = array('u', box_chars['bl'] + label_border + box_chars['br'])
textpad[y][x:x + w] = top_line
textpad[y + 1][x:x + w] = lab_line
for i in range(h - 3):
textpad[y + i + 2][x:x + w] = mid_line
textpad[y + h - 1][x:x + w] = bot_line
if x >= 0.25 and w == 1.25 and h == 2:
render_key_isoenter(textpad, x, y, w, h, label, style)
elif w == 2.25 and h == 2:
render_key_baenter(textpad, x, y, w, h, label, style)
else:
render_key_rect(textpad, x, y, w, h, label, style)
lines = []
for line in textpad:
@@ -216,3 +201,96 @@ def render_layouts(info_json, render_ascii):
layouts[layout] = render_layout(layout_data, render_ascii)
return layouts
def render_key_rect(textpad, x, y, w, h, label, style):
box_chars = BOX_DRAWING_CHARACTERS[style]
x = ceil(x * 4)
y = ceil(y * 3)
w = ceil(w * 4)
h = ceil(h * 3)
label_len = w - 2
label_leftover = label_len - len(label)
if len(label) > label_len:
label = label[:label_len]
label_blank = ' ' * label_len
label_border = box_chars['h'] * label_len
label_middle = label + ' '*label_leftover # noqa: yapf insists there be no whitespace around *
top_line = array('u', box_chars['tl'] + label_border + box_chars['tr'])
lab_line = array('u', box_chars['v'] + label_middle + box_chars['v'])
mid_line = array('u', box_chars['v'] + label_blank + box_chars['v'])
bot_line = array('u', box_chars['bl'] + label_border + box_chars['br'])
textpad[y][x:x + w] = top_line
textpad[y + 1][x:x + w] = lab_line
for i in range(h - 3):
textpad[y + i + 2][x:x + w] = mid_line
textpad[y + h - 1][x:x + w] = bot_line
def render_key_isoenter(textpad, x, y, w, h, label, style):
box_chars = BOX_DRAWING_CHARACTERS[style]
x = ceil(x * 4)
y = ceil(y * 3)
w = ceil(w * 4)
h = ceil(h * 3)
label_len = w - 1
label_leftover = label_len - len(label)
if len(label) > label_len:
label = label[:label_len]
label_blank = ' ' * (label_len-1) # noqa: yapf insists there be no whitespace around - and *
label_border_top = box_chars['h'] * label_len
label_border_bottom = box_chars['h'] * (label_len-1) # noqa
label_middle = label + ' '*label_leftover # noqa
top_line = array('u', box_chars['tl'] + label_border_top + box_chars['tr'])
lab_line = array('u', box_chars['v'] + label_middle + box_chars['v'])
crn_line = array('u', box_chars['bl'] + box_chars['tr'] + label_blank + box_chars['v'])
mid_line = array('u', box_chars['v'] + label_blank + box_chars['v'])
bot_line = array('u', box_chars['bl'] + label_border_bottom + box_chars['br'])
textpad[y][x - 1:x + w] = top_line
textpad[y + 1][x - 1:x + w] = lab_line
textpad[y + 2][x - 1:x + w] = crn_line
textpad[y + 3][x:x + w] = mid_line
textpad[y + 4][x:x + w] = mid_line
textpad[y + 5][x:x + w] = bot_line
def render_key_baenter(textpad, x, y, w, h, label, style):
box_chars = BOX_DRAWING_CHARACTERS[style]
x = ceil(x * 4)
y = ceil(y * 3)
w = ceil(w * 4)
h = ceil(h * 3)
label_len = w - 2
label_leftover = label_len - len(label)
if len(label) > label_len:
label = label[:label_len]
label_blank = ' ' * (label_len-3) # noqa: yapf insists there be no whitespace around - and *
label_border_top = box_chars['h'] * (label_len-3) # noqa
label_border_bottom = box_chars['h'] * label_len
label_middle = label + ' '*label_leftover # noqa
top_line = array('u', box_chars['tl'] + label_border_top + box_chars['tr'])
mid_line = array('u', box_chars['v'] + label_blank + box_chars['v'])
crn_line = array('u', box_chars['tl'] + box_chars['h'] + box_chars['h'] + box_chars['br'] + label_blank + box_chars['v'])
lab_line = array('u', box_chars['v'] + label_middle + box_chars['v'])
bot_line = array('u', box_chars['bl'] + label_border_bottom + box_chars['br'])
textpad[y][x + 3:x + w] = top_line
textpad[y + 1][x + 3:x + w] = mid_line
textpad[y + 2][x + 3:x + w] = mid_line
textpad[y + 3][x:x + w] = crn_line
textpad[y + 4][x:x + w] = lab_line
textpad[y + 5][x:x + w] = bot_line

View File

@@ -158,7 +158,7 @@ def is_keymap_dir(keymap, c=True, json=True, additional_files=None):
return True
def generate_json(keymap, keyboard, layout, layers):
def generate_json(keymap, keyboard, layout, layers, macros=None):
"""Returns a `keymap.json` for the specified keyboard, layout, and layers.
Args:
@@ -173,11 +173,16 @@ def generate_json(keymap, 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_json(keyboard)
new_keymap['keymap'] = keymap
new_keymap['layout'] = layout
new_keymap['layers'] = layers
if macros:
new_keymap['macros'] = macros
return new_keymap

View File

@@ -46,7 +46,7 @@ def keymap(keyboard_name):
"""
keyboard_folder = keyboard(keyboard_name)
for i in range(MAX_KEYBOARD_SUBFOLDERS):
for _ in range(MAX_KEYBOARD_SUBFOLDERS):
if (keyboard_folder / 'keymaps').exists():
return (keyboard_folder / 'keymaps').resolve()
@@ -70,9 +70,15 @@ def normpath(path):
class FileType(argparse.FileType):
def __init__(self, *args, **kwargs):
# Use UTF8 by default for stdin
if 'encoding' not in kwargs:
kwargs['encoding'] = 'UTF-8'
return super().__init__(*args, **kwargs)
def __call__(self, string):
"""normalize and check exists
otherwise magic strings like '-' for stdin resolve to bad paths
"""
norm = normpath(string)
return super().__call__(norm if norm.exists() else string)
return norm if norm.exists() else super().__call__(string)

View File

@@ -113,7 +113,11 @@ def test_list_keymaps_community():
def test_list_keymaps_kb_only():
<<<<<<< HEAD
result = check_subcommand('list-keymaps', '-kb', 'moonlander')
=======
result = check_subcommand('list-keymaps', '-kb', 'contra')
>>>>>>> qmk/master
check_returncode(result)
assert 'default' and 'oyrx' and 'webusb' in result.stdout
@@ -156,6 +160,18 @@ def test_json2c_stdin():
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_wrong_json():
result = check_subcommand('json2c', 'keyboards/handwired/pytest/info.json')
check_returncode(result, [1])
assert 'Invalid JSON keymap' in result.stdout
def test_json2c_no_json():
result = check_subcommand('json2c', 'keyboards/handwired/pytest/pytest.h')
check_returncode(result, [1])
assert 'Invalid JSON encountered' in result.stdout
def test_info():
result = check_subcommand('info', '-kb', 'handwired/pytest/basic')
check_returncode(result)