Browse Source

Generate api data on each push (#10609)

* add new qmk generate-api command, to generate a complete set of API data.

* Generate api data and push it to the keyboard repo

* fix typo

* Apply suggestions from code review

Co-authored-by: Joel Challis <git@zvecr.com>

* fixup api workflow

* remove file-changes-action

* use a more mainstream github action

* fix yaml error

* Apply suggestions from code review

Co-authored-by: Erovia <Erovia@users.noreply.github.com>

* more uniform date handling

* make flake8 happy

* Update lib/python/qmk/decorators.py

Co-authored-by: Erovia <Erovia@users.noreply.github.com>

Co-authored-by: Joel Challis <git@zvecr.com>
Co-authored-by: Erovia <Erovia@users.noreply.github.com>
pull/10768/head
Zach White 3 years ago
committed by GitHub
parent
commit
0c42f91f4c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 397 additions and 125 deletions
  1. +35
    -0
      .github/workflows/api.yml
  2. +1
    -0
      .gitignore
  3. +1
    -0
      api_data/_config.yml
  4. +5
    -0
      api_data/readme.md
  5. +1
    -0
      lib/python/qmk/cli/__init__.py
  6. +1
    -1
      lib/python/qmk/cli/c2json.py
  7. +1
    -0
      lib/python/qmk/cli/generate/__init__.py
  8. +58
    -0
      lib/python/qmk/cli/generate/api.py
  9. +28
    -28
      lib/python/qmk/cli/info.py
  10. +1
    -1
      lib/python/qmk/cli/json2c.py
  11. +2
    -17
      lib/python/qmk/cli/list/keyboards.py
  12. +5
    -0
      lib/python/qmk/constants.py
  13. +29
    -0
      lib/python/qmk/datetime.py
  14. +36
    -0
      lib/python/qmk/decorators.py
  15. +8
    -0
      lib/python/qmk/info.py
  16. +20
    -0
      lib/python/qmk/keyboard.py
  17. +153
    -66
      lib/python/qmk/keymap.py
  18. +12
    -12
      lib/python/qmk/tests/test_qmk_keymap.py

+ 35
- 0
.github/workflows/api.yml View File

@ -0,0 +1,35 @@
name: Update API Data
on:
push:
branches:
- master
paths:
- 'keyboards/**'
- 'layouts/community/**'
jobs:
api_data:
runs-on: ubuntu-latest
container: qmkfm/base_container
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 1
persist-credentials: false
- name: Generate API Data
run: qmk generate-api
- name: Upload API Data
uses: JamesIves/github-pages-deploy-action@3.7.1
with:
ACCESS_TOKEN: ${{ secrets.API_TOKEN_GITHUB }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: main
FOLDER: api_data/v1
CLEAN: true
GIT_CONFIG_EMAIL: hello@qmk.fm
REPOSITORY_NAME: qmk/qmk_keyboards
TARGET_FOLDER: v1

+ 1
- 0
.gitignore View File

@ -16,6 +16,7 @@
*.swp *.swp
tags tags
*~ *~
api_data/v1
build/ build/
.build/ .build/
*.bak *.bak


+ 1
- 0
api_data/_config.yml View File

@ -0,0 +1 @@
theme: jekyll-theme-cayman

+ 5
- 0
api_data/readme.md View File

@ -0,0 +1,5 @@
# QMK Keyboard Metadata
This directory contains machine parsable data about keyboards supported by QMK. The latest version is always available online at <https://keyboards.qmk.fm>.
Do not edit anything here by hand. It is generated with the `qmk generate-api` command.

+ 1
- 0
lib/python/qmk/cli/__init__.py View File

@ -13,6 +13,7 @@ from . import config
from . import docs from . import docs
from . import doctor from . import doctor
from . import flash from . import flash
from . import generate
from . import hello from . import hello
from . import info from . import info
from . import json from . import json


+ 1
- 1
lib/python/qmk/cli/c2json.py View File

@ -44,7 +44,7 @@ def c2json(cli):
# Generate the keymap.json # Generate the keymap.json
try: try:
keymap_json = qmk.keymap.generate(keymap_json['keyboard'], keymap_json['layout'], keymap_json['layers'], type='json', keymap=keymap_json['keymap'])
keymap_json = qmk.keymap.generate_json(keymap_json['keymap'], keymap_json['keyboard'], keymap_json['layout'], keymap_json['layers'])
except KeyError: except KeyError:
cli.log.error('Something went wrong. Try to use --no-cpp.') cli.log.error('Something went wrong. Try to use --no-cpp.')
sys.exit(1) sys.exit(1)


+ 1
- 0
lib/python/qmk/cli/generate/__init__.py View File

@ -0,0 +1 @@
from . import api

+ 58
- 0
lib/python/qmk/cli/generate/api.py View File

@ -0,0 +1,58 @@
"""This script automates the generation of the QMK API data.
"""
from pathlib import Path
from shutil import copyfile
import json
from milc import cli
from qmk.datetime import current_datetime
from qmk.info import info_json
from qmk.keyboard import list_keyboards
@cli.subcommand('Creates a new keymap for the keyboard of your choosing', hidden=False if cli.config.user.developer else True)
def generate_api(cli):
"""Generates the QMK API data.
"""
api_data_dir = Path('api_data')
v1_dir = api_data_dir / 'v1'
keyboard_list = v1_dir / 'keyboard_list.json'
keyboard_all = v1_dir / 'keyboards.json'
usb_file = v1_dir / 'usb.json'
if not api_data_dir.exists():
api_data_dir.mkdir()
kb_all = {'last_updated': current_datetime(), 'keyboards': {}}
usb_list = {'last_updated': current_datetime(), 'devices': {}}
# Generate and write keyboard specific JSON files
for keyboard_name in list_keyboards():
kb_all['keyboards'][keyboard_name] = info_json(keyboard_name)
keyboard_dir = v1_dir / 'keyboards' / keyboard_name
keyboard_info = keyboard_dir / 'info.json'
keyboard_readme = keyboard_dir / 'readme.md'
keyboard_readme_src = Path('keyboards') / keyboard_name / 'readme.md'
keyboard_dir.mkdir(parents=True, exist_ok=True)
keyboard_info.write_text(json.dumps(kb_all['keyboards'][keyboard_name]))
if keyboard_readme_src.exists():
copyfile(keyboard_readme_src, keyboard_readme)
if 'usb' in kb_all['keyboards'][keyboard_name]:
usb = kb_all['keyboards'][keyboard_name]['usb']
if usb['vid'] not in usb_list['devices']:
usb_list['devices'][usb['vid']] = {}
if usb['pid'] not in usb_list['devices'][usb['vid']]:
usb_list['devices'][usb['vid']][usb['pid']] = {}
usb_list['devices'][usb['vid']][usb['pid']][keyboard_name] = usb
# Write the global JSON files
keyboard_list.write_text(json.dumps({'last_updated': current_datetime(), 'keyboards': sorted(kb_all['keyboards'])}))
keyboard_all.write_text(json.dumps(kb_all))
usb_file.write_text(json.dumps(usb_list))

+ 28
- 28
lib/python/qmk/cli/info.py View File

@ -16,7 +16,7 @@ ROW_LETTERS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop'
COL_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijilmnopqrstuvwxyz' COL_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijilmnopqrstuvwxyz'
def show_keymap(info_json, title_caps=True):
def show_keymap(kb_info_json, title_caps=True):
"""Render the keymap in ascii art. """Render the keymap in ascii art.
""" """
keymap_path = locate_keymap(cli.config.info.keyboard, cli.config.info.keymap) keymap_path = locate_keymap(cli.config.info.keyboard, cli.config.info.keymap)
@ -36,7 +36,7 @@ def show_keymap(info_json, title_caps=True):
else: else:
cli.echo('{fg_cyan}layer_%s{fg_reset}:', layer_num) cli.echo('{fg_cyan}layer_%s{fg_reset}:', layer_num)
print(render_layout(info_json['layouts'][layout_name]['layout'], layer))
print(render_layout(kb_info_json['layouts'][layout_name]['layout'], layer))
def show_layouts(kb_info_json, title_caps=True): def show_layouts(kb_info_json, title_caps=True):
@ -48,10 +48,10 @@ def show_layouts(kb_info_json, title_caps=True):
print(layout_art) # Avoid passing dirty data to cli.echo() print(layout_art) # Avoid passing dirty data to cli.echo()
def show_matrix(info_json, title_caps=True):
def show_matrix(kb_info_json, title_caps=True):
"""Render the layout with matrix labels in ascii art. """Render the layout with matrix labels in ascii art.
""" """
for layout_name, layout in info_json['layouts'].items():
for layout_name, layout in kb_info_json['layouts'].items():
# Build our label list # Build our label list
labels = [] labels = []
for key in layout['layout']: for key in layout['layout']:
@ -69,54 +69,54 @@ def show_matrix(info_json, title_caps=True):
else: else:
cli.echo('{fg_blue}matrix_%s{fg_reset}:', layout_name) cli.echo('{fg_blue}matrix_%s{fg_reset}:', layout_name)
print(render_layout(info_json['layouts'][layout_name]['layout'], labels))
print(render_layout(kb_info_json['layouts'][layout_name]['layout'], labels))
def print_friendly_output(info_json):
def print_friendly_output(kb_info_json):
"""Print the info.json in a friendly text format. """Print the info.json in a friendly text format.
""" """
cli.echo('{fg_blue}Keyboard Name{fg_reset}: %s', info_json.get('keyboard_name', 'Unknown'))
cli.echo('{fg_blue}Manufacturer{fg_reset}: %s', info_json.get('manufacturer', 'Unknown'))
if 'url' in info_json:
cli.echo('{fg_blue}Website{fg_reset}: %s', info_json.get('url', ''))
if info_json.get('maintainer', 'qmk') == 'qmk':
cli.echo('{fg_blue}Keyboard Name{fg_reset}: %s', kb_info_json.get('keyboard_name', 'Unknown'))
cli.echo('{fg_blue}Manufacturer{fg_reset}: %s', kb_info_json.get('manufacturer', 'Unknown'))
if 'url' in kb_info_json:
cli.echo('{fg_blue}Website{fg_reset}: %s', kb_info_json.get('url', ''))
if kb_info_json.get('maintainer', 'qmk') == 'qmk':
cli.echo('{fg_blue}Maintainer{fg_reset}: QMK Community') cli.echo('{fg_blue}Maintainer{fg_reset}: QMK Community')
else: else:
cli.echo('{fg_blue}Maintainer{fg_reset}: %s', info_json['maintainer'])
cli.echo('{fg_blue}Keyboard Folder{fg_reset}: %s', info_json.get('keyboard_folder', 'Unknown'))
cli.echo('{fg_blue}Layouts{fg_reset}: %s', ', '.join(sorted(info_json['layouts'].keys())))
if 'width' in info_json and 'height' in info_json:
cli.echo('{fg_blue}Size{fg_reset}: %s x %s' % (info_json['width'], info_json['height']))
cli.echo('{fg_blue}Processor{fg_reset}: %s', info_json.get('processor', 'Unknown'))
cli.echo('{fg_blue}Bootloader{fg_reset}: %s', info_json.get('bootloader', 'Unknown'))
cli.echo('{fg_blue}Maintainer{fg_reset}: %s', kb_info_json['maintainer'])
cli.echo('{fg_blue}Keyboard Folder{fg_reset}: %s', kb_info_json.get('keyboard_folder', 'Unknown'))
cli.echo('{fg_blue}Layouts{fg_reset}: %s', ', '.join(sorted(kb_info_json['layouts'].keys())))
if 'width' in kb_info_json and 'height' in kb_info_json:
cli.echo('{fg_blue}Size{fg_reset}: %s x %s' % (kb_info_json['width'], kb_info_json['height']))
cli.echo('{fg_blue}Processor{fg_reset}: %s', kb_info_json.get('processor', 'Unknown'))
cli.echo('{fg_blue}Bootloader{fg_reset}: %s', kb_info_json.get('bootloader', 'Unknown'))
if cli.config.info.layouts: if cli.config.info.layouts:
show_layouts(info_json, True)
show_layouts(kb_info_json, True)
if cli.config.info.matrix: if cli.config.info.matrix:
show_matrix(info_json, True)
show_matrix(kb_info_json, True)
if cli.config_source.info.keymap and cli.config_source.info.keymap != 'config_file': if cli.config_source.info.keymap and cli.config_source.info.keymap != 'config_file':
show_keymap(info_json, True)
show_keymap(kb_info_json, True)
def print_text_output(info_json):
def print_text_output(kb_info_json):
"""Print the info.json in a plain text format. """Print the info.json in a plain text format.
""" """
for key in sorted(info_json):
for key in sorted(kb_info_json):
if key == 'layouts': if key == 'layouts':
cli.echo('{fg_blue}layouts{fg_reset}: %s', ', '.join(sorted(info_json['layouts'].keys())))
cli.echo('{fg_blue}layouts{fg_reset}: %s', ', '.join(sorted(kb_info_json['layouts'].keys())))
else: else:
cli.echo('{fg_blue}%s{fg_reset}: %s', key, info_json[key])
cli.echo('{fg_blue}%s{fg_reset}: %s', key, kb_info_json[key])
if cli.config.info.layouts: if cli.config.info.layouts:
show_layouts(info_json, False)
show_layouts(kb_info_json, False)
if cli.config.info.matrix: if cli.config.info.matrix:
show_matrix(info_json, False)
show_matrix(kb_info_json, False)
if cli.config_source.info.keymap and cli.config_source.info.keymap != 'config_file': if cli.config_source.info.keymap and cli.config_source.info.keymap != 'config_file':
show_keymap(info_json, False)
show_keymap(kb_info_json, False)
@cli.argument('-kb', '--keyboard', help='Keyboard to show info for.') @cli.argument('-kb', '--keyboard', help='Keyboard to show info for.')


+ 1
- 1
lib/python/qmk/cli/json2c.py View File

@ -38,7 +38,7 @@ def json2c(cli):
user_keymap = json.load(fd) user_keymap = json.load(fd)
# Generate the keymap # Generate the keymap
keymap_c = qmk.keymap.generate(user_keymap['keyboard'], user_keymap['layout'], user_keymap['layers'])
keymap_c = qmk.keymap.generate_c(user_keymap['keyboard'], user_keymap['layout'], user_keymap['layers'])
if cli.args.output: if cli.args.output:
cli.args.output.parent.mkdir(parents=True, exist_ok=True) cli.args.output.parent.mkdir(parents=True, exist_ok=True)


+ 2
- 17
lib/python/qmk/cli/list/keyboards.py View File

@ -1,28 +1,13 @@
"""List the keyboards currently defined within QMK """List the keyboards currently defined within QMK
""" """
# We avoid pathlib here because this is performance critical code.
import os
import glob
from milc import cli from milc import cli
BASE_PATH = os.path.join(os.getcwd(), "keyboards") + os.path.sep
KB_WILDCARD = os.path.join(BASE_PATH, "**", "rules.mk")
def find_name(path):
"""Determine the keyboard name by stripping off the base_path and rules.mk.
"""
return path.replace(BASE_PATH, "").replace(os.path.sep + "rules.mk", "")
import qmk.keyboard
@cli.subcommand("List the keyboards currently defined within QMK") @cli.subcommand("List the keyboards currently defined within QMK")
def list_keyboards(cli): def list_keyboards(cli):
"""List the keyboards currently defined within QMK """List the keyboards currently defined within QMK
""" """
# find everywhere we have rules.mk where keymaps isn't in the path
paths = [path for path in glob.iglob(KB_WILDCARD, recursive=True) if 'keymaps' not in path]
# Extract the keyboard name from the path and print it
for keyboard_name in sorted(map(find_name, paths)):
for keyboard_name in qmk.keyboard.list_keyboards():
print(keyboard_name) print(keyboard_name)

+ 5
- 0
lib/python/qmk/constants.py View File

@ -12,3 +12,8 @@ MAX_KEYBOARD_SUBFOLDERS = 5
CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F411' CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F411'
LUFA_PROCESSORS = 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None LUFA_PROCESSORS = 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None
VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85' VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85'
# Common format strings
DATE_FORMAT = '%Y-%m-%d'
DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S %Z'
TIME_FORMAT = '%H:%M:%S'

+ 29
- 0
lib/python/qmk/datetime.py View File

@ -0,0 +1,29 @@
"""Functions to work with dates and times in a uniform way.
The results of these functions are cached for 5 seconds to provide uniform time strings across short running processes. Long running processes that need more precise timekeeping should not use these functions.
"""
from time import gmtime, strftime
from qmk.constants import DATE_FORMAT, DATETIME_FORMAT, TIME_FORMAT
from qmk.decorators import lru_cache
@lru_cache(timeout=5)
def current_date():
"""Returns the current time in UTZ as a formatted string.
"""
return strftime(DATE_FORMAT, gmtime())
@lru_cache(timeout=5)
def current_datetime():
"""Returns the current time in UTZ as a formatted string.
"""
return strftime(DATETIME_FORMAT, gmtime())
@lru_cache(timeout=5)
def current_time():
"""Returns the current time in UTZ as a formatted string.
"""
return strftime(TIME_FORMAT, gmtime())

+ 36
- 0
lib/python/qmk/decorators.py View File

@ -2,6 +2,7 @@
""" """
import functools import functools
from pathlib import Path from pathlib import Path
from time import monotonic
from milc import cli from milc import cli
@ -84,3 +85,38 @@ def automagic_keymap(func):
return func(*args, **kwargs) return func(*args, **kwargs)
return wrapper return wrapper
def lru_cache(timeout=10, maxsize=128, typed=False):
"""Least Recently Used Cache- cache the result of a function.
Args:
timeout
How many seconds to cache results for.
maxsize
The maximum size of the cache in bytes
typed
When `True` argument types will be taken into consideration, for example `3` and `3.0` will be treated as different keys.
"""
def wrapper_cache(func):
func = functools.lru_cache(maxsize=maxsize, typed=typed)(func)
func.expiration = monotonic() + timeout
@functools.wraps(func)
def wrapped_func(*args, **kwargs):
if monotonic() >= func.expiration:
func.expiration = monotonic() + timeout
func.cache_clear()
return func(*args, **kwargs)
wrapped_func.cache_info = func.cache_info
wrapped_func.cache_clear = func.cache_clear
return wrapped_func
return wrapper_cache

+ 8
- 0
lib/python/qmk/info.py View File

@ -9,6 +9,7 @@ from milc import cli
from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS
from qmk.c_parse import find_layouts from qmk.c_parse import find_layouts
from qmk.keyboard import config_h, rules_mk from qmk.keyboard import config_h, rules_mk
from qmk.keymap import list_keymaps
from qmk.makefile import parse_rules_mk_file from qmk.makefile import parse_rules_mk_file
from qmk.math import compute from qmk.math import compute
@ -25,14 +26,21 @@ def info_json(keyboard):
info_data = { info_data = {
'keyboard_name': str(keyboard), 'keyboard_name': str(keyboard),
'keyboard_folder': str(keyboard), 'keyboard_folder': str(keyboard),
'keymaps': {},
'layouts': {}, 'layouts': {},
'maintainer': 'qmk', 'maintainer': 'qmk',
} }
# Populate the list of JSON keymaps
for keymap in list_keymaps(keyboard, c=False, fullpath=True):
info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'}
# Populate layout data
for layout_name, layout_json in _find_all_layouts(keyboard, rules).items(): for layout_name, layout_json in _find_all_layouts(keyboard, rules).items():
if not layout_name.startswith('LAYOUT_kc'): if not layout_name.startswith('LAYOUT_kc'):
info_data['layouts'][layout_name] = layout_json info_data['layouts'][layout_name] = layout_json
# Merge in the data from info.json, config.h, and rules.mk
info_data = merge_info_jsons(keyboard, info_data) info_data = merge_info_jsons(keyboard, info_data)
info_data = _extract_config_h(info_data) info_data = _extract_config_h(info_data)
info_data = _extract_rules_mk(info_data) info_data = _extract_rules_mk(info_data)


+ 20
- 0
lib/python/qmk/keyboard.py View File

@ -3,10 +3,30 @@
from array import array from array import array
from math import ceil from math import ceil
from pathlib import Path from pathlib import Path
import os
from glob import glob
from qmk.c_parse import parse_config_h_file from qmk.c_parse import parse_config_h_file
from qmk.makefile import parse_rules_mk_file from qmk.makefile import parse_rules_mk_file
base_path = os.path.join(os.getcwd(), "keyboards") + os.path.sep
def _find_name(path):
"""Determine the keyboard name by stripping off the base_path and rules.mk.
"""
return path.replace(base_path, "").replace(os.path.sep + "rules.mk", "")
def list_keyboards():
"""Returns a list of all keyboards.
"""
# We avoid pathlib here because this is performance critical code.
kb_wildcard = os.path.join(base_path, "**", "rules.mk")
paths = [path for path in glob(kb_wildcard, recursive=True) if 'keymaps' not in path]
return sorted(map(_find_name, paths))
def config_h(keyboard): def config_h(keyboard):
"""Parses all the config.h files for a keyboard. """Parses all the config.h files for a keyboard.


+ 153
- 66
lib/python/qmk/keymap.py View File

@ -29,33 +29,37 @@ __KEYMAP_GOES_HERE__
""" """
def template(keyboard, type='c'):
"""Returns the `keymap.c` or `keymap.json` template for a keyboard.
def template_json(keyboard):
"""Returns a `keymap.json` template for a keyboard.
If a template exists in `keyboards/<keyboard>/templates/keymap.c` that
text will be used instead of `DEFAULT_KEYMAP_C`.
If a template exists in `keyboards/<keyboard>/templates/keymap.json` that
text will be used instead of an empty dictionary.
If a template exists in `keyboards/<keyboard>/templates/keymap.json` that text will be used instead of an empty dictionary.
Args: Args:
keyboard keyboard
The keyboard to return a template for. The keyboard to return a template for.
"""
template_file = Path('keyboards/%s/templates/keymap.json' % keyboard)
template = {'keyboard': keyboard}
if template_file.exists():
template.update(json.loads(template_file.read_text()))
return template
type
'json' for `keymap.json` and 'c' (or anything else) for `keymap.c`
def template_c(keyboard):
"""Returns a `keymap.c` template for a keyboard.
If a template exists in `keyboards/<keyboard>/templates/keymap.c` that text will be used instead of an empty dictionary.
Args:
keyboard
The keyboard to return a template for.
""" """
if type == 'json':
template_file = Path('keyboards/%s/templates/keymap.json' % keyboard)
template = {'keyboard': keyboard}
if template_file.exists():
template.update(json.loads(template_file.read_text()))
template_file = Path('keyboards/%s/templates/keymap.c' % keyboard)
if template_file.exists():
template = template_file.read_text()
else: else:
template_file = Path('keyboards/%s/templates/keymap.c' % keyboard)
if template_file.exists():
template = template_file.read_text()
else:
template = DEFAULT_KEYMAP_C
template = DEFAULT_KEYMAP_C
return template return template
@ -69,15 +73,65 @@ def _strip_any(keycode):
return keycode return keycode
def is_keymap_dir(keymap):
def is_keymap_dir(keymap, c=True, json=True, additional_files=None):
"""Return True if Path object `keymap` has a keymap file inside. """Return True if Path object `keymap` has a keymap file inside.
Args:
keymap
A Path() object for the keymap directory you want to check.
c
When true include `keymap.c` keymaps.
json
When true include `keymap.json` keymaps.
additional_files
A sequence of additional filenames to check against to determine if a directory is a keymap. All files must exist for a match to happen. For example, if you want to match a C keymap with both a `config.h` and `rules.mk` file: `is_keymap_dir(keymap_dir, json=False, additional_files=['config.h', 'rules.mk'])`
""" """
for file in ('keymap.c', 'keymap.json'):
files = []
if c:
files.append('keymap.c')
if json:
files.append('keymap.json')
for file in files:
if (keymap / file).is_file(): if (keymap / file).is_file():
if additional_files:
for file in additional_files:
if not (keymap / file).is_file():
return False
return True return True
def generate(keyboard, layout, layers, type='c', keymap=None):
def generate_json(keymap, keyboard, layout, layers):
"""Returns a `keymap.json` for the specified keyboard, layout, and layers.
Args:
keymap
A name for this keymap.
keyboard
The name of the keyboard.
layout
The LAYOUT macro this keymap uses.
layers
An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode.
"""
new_keymap = template_json(keyboard)
new_keymap['keymap'] = keymap
new_keymap['layout'] = layout
new_keymap['layers'] = layers
return new_keymap
def generate_c(keyboard, layout, layers):
"""Returns a `keymap.c` or `keymap.json` for the specified keyboard, layout, and layers. """Returns a `keymap.c` or `keymap.json` for the specified keyboard, layout, and layers.
Args: Args:
@ -89,33 +143,33 @@ def generate(keyboard, layout, layers, type='c', keymap=None):
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. An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode.
type
'json' for `keymap.json` and 'c' (or anything else) for `keymap.c`
""" """
new_keymap = template(keyboard, type)
if type == 'json':
new_keymap['keymap'] = keymap
new_keymap['layout'] = layout
new_keymap['layers'] = layers
else:
layer_txt = []
for layer_num, layer in enumerate(layers):
if layer_num != 0:
layer_txt[-1] = layer_txt[-1] + ','
new_keymap = template_c(keyboard)
layer_txt = []
for layer_num, layer in enumerate(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))
keymap = '\n'.join(layer_txt)
new_keymap = new_keymap.replace('__KEYMAP_GOES_HERE__', keymap)
layer = map(_strip_any, layer)
layer_keys = ', '.join(layer)
layer_txt.append('\t[%s] = %s(%s)' % (layer_num, layout, layer_keys))
return new_keymap
keymap = '\n'.join(layer_txt)
new_keymap = new_keymap.replace('__KEYMAP_GOES_HERE__', keymap)
return new_keymap
def write_file(keymap_filename, keymap_content):
keymap_filename.parent.mkdir(parents=True, exist_ok=True)
keymap_filename.write_text(keymap_content)
cli.log.info('Wrote keymap to {fg_cyan}%s', keymap_filename)
return keymap_filename
def write(keyboard, keymap, layout, layers, type='c'):
"""Generate the `keymap.c` and write it to disk.
def write_json(keyboard, keymap, layout, layers):
"""Generate the `keymap.json` and write it to disk.
Returns the filename written to. Returns the filename written to.
@ -131,23 +185,36 @@ def write(keyboard, keymap, layout, layers, type='c'):
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. An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode.
type
'json' for `keymap.json` and 'c' (or anything else) for `keymap.c`
""" """
keymap_content = generate(keyboard, layout, layers, type)
if type == 'json':
keymap_file = qmk.path.keymap(keyboard) / keymap / 'keymap.json'
keymap_content = json.dumps(keymap_content)
else:
keymap_file = qmk.path.keymap(keyboard) / keymap / 'keymap.c'
keymap_json = generate_json(keyboard, keymap, layout, layers)
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):
"""Generate the `keymap.c` and write it to disk.
Returns the filename written to.
Args:
keyboard
The name of the keyboard
keymap_file.parent.mkdir(parents=True, exist_ok=True)
keymap_file.write_text(keymap_content)
keymap
The name of the keymap
cli.log.info('Wrote keymap to {fg_cyan}%s', keymap_file)
layout
The LAYOUT macro this keymap uses.
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_content = generate_c(keyboard, layout, layers)
keymap_file = qmk.path.keymap(keyboard) / keymap / 'keymap.c'
return keymap_file
return write_file(keymap_file, keymap_content)
def locate_keymap(keyboard, keymap): def locate_keymap(keyboard, keymap):
@ -189,38 +256,58 @@ def locate_keymap(keyboard, keymap):
return community_layout / 'keymap.c' return community_layout / 'keymap.c'
def list_keymaps(keyboard):
""" List the available keymaps for a keyboard.
def list_keymaps(keyboard, c=True, json=True, additional_files=None, fullpath=False):
"""List the available keymaps for a keyboard.
Args: Args:
keyboard: the keyboards full name with vendor and revision if necessary, example: clueboard/66/rev3
keyboard
The keyboards full name with vendor and revision if necessary, example: clueboard/66/rev3
c
When true include `keymap.c` keymaps.
json
When true include `keymap.json` keymaps.
additional_files
A sequence of additional filenames to check against to determine if a directory is a keymap. All files must exist for a match to happen. For example, if you want to match a C keymap with both a `config.h` and `rules.mk` file: `is_keymap_dir(keymap_dir, json=False, additional_files=['config.h', 'rules.mk'])`
fullpath
When set to True the full path of the keymap relative to the `qmk_firmware` root will be provided.
Returns: Returns:
a set with the names of the available keymaps
a sorted list of valid keymap names.
""" """
# parse all the rules.mk files for the keyboard # parse all the rules.mk files for the keyboard
rules = rules_mk(keyboard) rules = rules_mk(keyboard)
names = set() names = set()
if rules: if rules:
# qmk_firmware/keyboards
keyboards_dir = Path('keyboards') keyboards_dir = Path('keyboards')
# path to the keyboard's directory
kb_path = keyboards_dir / keyboard kb_path = keyboards_dir / keyboard
# walk up the directory tree until keyboards_dir # walk up the directory tree until keyboards_dir
# and collect all directories' name with keymap.c file in it # and collect all directories' name with keymap.c file in it
while kb_path != keyboards_dir: while kb_path != keyboards_dir:
keymaps_dir = kb_path / "keymaps" keymaps_dir = kb_path / "keymaps"
if keymaps_dir.exists():
names = names.union([keymap.name for keymap in keymaps_dir.iterdir() if is_keymap_dir(keymap)])
if keymaps_dir.is_dir():
for keymap in keymaps_dir.iterdir():
if is_keymap_dir(keymap, c, json, additional_files):
keymap = keymap if fullpath else keymap.name
names.add(keymap)
kb_path = kb_path.parent kb_path = kb_path.parent
# if community layouts are supported, get them # if community layouts are supported, get them
if "LAYOUTS" in rules: if "LAYOUTS" in rules:
for layout in rules["LAYOUTS"].split(): for layout in rules["LAYOUTS"].split():
cl_path = Path('layouts/community') / layout cl_path = Path('layouts/community') / layout
if cl_path.exists():
names = names.union([keymap.name for keymap in cl_path.iterdir() if is_keymap_dir(keymap)])
if cl_path.is_dir():
for keymap in cl_path.iterdir():
if is_keymap_dir(keymap, c, json, additional_files):
keymap = keymap if fullpath else keymap.name
names.add(keymap)
return sorted(names) return sorted(names)


+ 12
- 12
lib/python/qmk/tests/test_qmk_keymap.py View File

@ -1,33 +1,33 @@
import qmk.keymap import qmk.keymap
def test_template_onekey_proton_c():
templ = qmk.keymap.template('handwired/onekey/proton_c')
def test_template_c_onekey_proton_c():
templ = qmk.keymap.template_c('handwired/onekey/proton_c')
assert templ == qmk.keymap.DEFAULT_KEYMAP_C assert templ == qmk.keymap.DEFAULT_KEYMAP_C
def test_template_onekey_proton_c_json():
templ = qmk.keymap.template('handwired/onekey/proton_c', type='json')
def test_template_json_onekey_proton_c():
templ = qmk.keymap.template_json('handwired/onekey/proton_c')
assert templ == {'keyboard': 'handwired/onekey/proton_c'} assert templ == {'keyboard': 'handwired/onekey/proton_c'}
def test_template_onekey_pytest():
templ = qmk.keymap.template('handwired/onekey/pytest')
def test_template_c_onekey_pytest():
templ = qmk.keymap.template_c('handwired/onekey/pytest')
assert templ == '#include QMK_KEYBOARD_H\nconst uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {__KEYMAP_GOES_HERE__};\n' assert templ == '#include QMK_KEYBOARD_H\nconst uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {__KEYMAP_GOES_HERE__};\n'
def test_template_onekey_pytest_json():
templ = qmk.keymap.template('handwired/onekey/pytest', type='json')
def test_template_json_onekey_pytest():
templ = qmk.keymap.template_json('handwired/onekey/pytest')
assert templ == {'keyboard': 'handwired/onekey/pytest', "documentation": "This file is a keymap.json file for handwired/onekey/pytest"} assert templ == {'keyboard': 'handwired/onekey/pytest', "documentation": "This file is a keymap.json file for handwired/onekey/pytest"}
def test_generate_onekey_pytest():
templ = qmk.keymap.generate('handwired/onekey/pytest', 'LAYOUT', [['KC_A']])
def test_generate_c_onekey_pytest():
templ = qmk.keymap.generate_c('handwired/onekey/pytest', 'LAYOUT', [['KC_A']])
assert templ == '#include QMK_KEYBOARD_H\nconst uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {\t[0] = LAYOUT(KC_A)};\n' assert templ == '#include QMK_KEYBOARD_H\nconst uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {\t[0] = LAYOUT(KC_A)};\n'
def test_generate_onekey_pytest_json():
templ = qmk.keymap.generate('handwired/onekey/pytest', 'LAYOUT', [['KC_A']], type='json', keymap='default')
def test_generate_json_onekey_pytest():
templ = qmk.keymap.generate_json('default', 'handwired/onekey/pytest', 'LAYOUT', [['KC_A']])
assert templ == {"keyboard": "handwired/onekey/pytest", "documentation": "This file is a keymap.json file for handwired/onekey/pytest", "keymap": "default", "layout": "LAYOUT", "layers": [["KC_A"]]} assert templ == {"keyboard": "handwired/onekey/pytest", "documentation": "This file is a keymap.json file for handwired/onekey/pytest", "keymap": "default", "layout": "LAYOUT", "layers": [["KC_A"]]}


Loading…
Cancel
Save