aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZach White <skullydazed@gmail.com>2020-10-25 14:48:44 -0700
committerGitHub <noreply@github.com>2020-10-25 14:48:44 -0700
commit0c42f91f4ccf98a37f055afb777ed491da56335e (patch)
tree547344d80fe7bf75ff3f348eefbc19dbdd346a8a
parent8ef82c466e73e555fd74107d4c57e678d7152ecc (diff)
downloadqmk_firmware-0c42f91f4ccf98a37f055afb777ed491da56335e.tar.gz
qmk_firmware-0c42f91f4ccf98a37f055afb777ed491da56335e.zip
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>
-rw-r--r--.github/workflows/api.yml35
-rw-r--r--.gitignore1
-rw-r--r--api_data/_config.yml1
-rw-r--r--api_data/readme.md5
-rw-r--r--lib/python/qmk/cli/__init__.py1
-rw-r--r--lib/python/qmk/cli/c2json.py2
-rw-r--r--lib/python/qmk/cli/generate/__init__.py1
-rwxr-xr-xlib/python/qmk/cli/generate/api.py58
-rwxr-xr-xlib/python/qmk/cli/info.py56
-rwxr-xr-xlib/python/qmk/cli/json2c.py2
-rw-r--r--lib/python/qmk/cli/list/keyboards.py19
-rw-r--r--lib/python/qmk/constants.py5
-rw-r--r--lib/python/qmk/datetime.py29
-rw-r--r--lib/python/qmk/decorators.py36
-rw-r--r--lib/python/qmk/info.py8
-rw-r--r--lib/python/qmk/keyboard.py20
-rw-r--r--lib/python/qmk/keymap.py219
-rw-r--r--lib/python/qmk/tests/test_qmk_keymap.py24
18 files changed, 397 insertions, 125 deletions
diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml
new file mode 100644
index 000000000..7a7bf75d0
--- /dev/null
+++ b/.github/workflows/api.yml
@@ -0,0 +1,35 @@
1name: Update API Data
2
3on:
4 push:
5 branches:
6 - master
7 paths:
8 - 'keyboards/**'
9 - 'layouts/community/**'
10
11jobs:
12 api_data:
13 runs-on: ubuntu-latest
14 container: qmkfm/base_container
15
16 steps:
17 - uses: actions/checkout@v2
18 with:
19 fetch-depth: 1
20 persist-credentials: false
21
22 - name: Generate API Data
23 run: qmk generate-api
24
25 - name: Upload API Data
26 uses: JamesIves/github-pages-deploy-action@3.7.1
27 with:
28 ACCESS_TOKEN: ${{ secrets.API_TOKEN_GITHUB }}
29 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30 BRANCH: main
31 FOLDER: api_data/v1
32 CLEAN: true
33 GIT_CONFIG_EMAIL: hello@qmk.fm
34 REPOSITORY_NAME: qmk/qmk_keyboards
35 TARGET_FOLDER: v1
diff --git a/.gitignore b/.gitignore
index 91d283e69..d6846cf63 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,7 @@
16*.swp 16*.swp
17tags 17tags
18*~ 18*~
19api_data/v1
19build/ 20build/
20.build/ 21.build/
21*.bak 22*.bak
diff --git a/api_data/_config.yml b/api_data/_config.yml
new file mode 100644
index 000000000..277f1f2c5
--- /dev/null
+++ b/api_data/_config.yml
@@ -0,0 +1 @@
theme: jekyll-theme-cayman
diff --git a/api_data/readme.md b/api_data/readme.md
new file mode 100644
index 000000000..a4b2c6bce
--- /dev/null
+++ b/api_data/readme.md
@@ -0,0 +1,5 @@
1# QMK Keyboard Metadata
2
3This directory contains machine parsable data about keyboards supported by QMK. The latest version is always available online at <https://keyboards.qmk.fm>.
4
5Do not edit anything here by hand. It is generated with the `qmk generate-api` command.
diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py
index ba964ebbb..47e1b4435 100644
--- a/lib/python/qmk/cli/__init__.py
+++ b/lib/python/qmk/cli/__init__.py
@@ -13,6 +13,7 @@ from . import config
13from . import docs 13from . import docs
14from . import doctor 14from . import doctor
15from . import flash 15from . import flash
16from . import generate
16from . import hello 17from . import hello
17from . import info 18from . import info
18from . import json 19from . import json
diff --git a/lib/python/qmk/cli/c2json.py b/lib/python/qmk/cli/c2json.py
index 0267303fd..8c8bd1f57 100644
--- a/lib/python/qmk/cli/c2json.py
+++ b/lib/python/qmk/cli/c2json.py
@@ -44,7 +44,7 @@ def c2json(cli):
44 44
45 # Generate the keymap.json 45 # Generate the keymap.json
46 try: 46 try:
47 keymap_json = qmk.keymap.generate(keymap_json['keyboard'], keymap_json['layout'], keymap_json['layers'], type='json', keymap=keymap_json['keymap']) 47 keymap_json = qmk.keymap.generate_json(keymap_json['keymap'], keymap_json['keyboard'], keymap_json['layout'], keymap_json['layers'])
48 except KeyError: 48 except KeyError:
49 cli.log.error('Something went wrong. Try to use --no-cpp.') 49 cli.log.error('Something went wrong. Try to use --no-cpp.')
50 sys.exit(1) 50 sys.exit(1)
diff --git a/lib/python/qmk/cli/generate/__init__.py b/lib/python/qmk/cli/generate/__init__.py
new file mode 100644
index 000000000..4dc7607ef
--- /dev/null
+++ b/lib/python/qmk/cli/generate/__init__.py
@@ -0,0 +1 @@
from . import api
diff --git a/lib/python/qmk/cli/generate/api.py b/lib/python/qmk/cli/generate/api.py
new file mode 100755
index 000000000..9807a9cd6
--- /dev/null
+++ b/lib/python/qmk/cli/generate/api.py
@@ -0,0 +1,58 @@
1"""This script automates the generation of the QMK API data.
2"""
3from pathlib import Path
4from shutil import copyfile
5import json
6
7from milc import cli
8
9from qmk.datetime import current_datetime
10from qmk.info import info_json
11from qmk.keyboard import list_keyboards
12
13
14@cli.subcommand('Creates a new keymap for the keyboard of your choosing', hidden=False if cli.config.user.developer else True)
15def generate_api(cli):
16 """Generates the QMK API data.
17 """
18 api_data_dir = Path('api_data')
19 v1_dir = api_data_dir / 'v1'
20 keyboard_list = v1_dir / 'keyboard_list.json'
21 keyboard_all = v1_dir / 'keyboards.json'
22 usb_file = v1_dir / 'usb.json'
23
24 if not api_data_dir.exists():
25 api_data_dir.mkdir()
26
27 kb_all = {'last_updated': current_datetime(), 'keyboards': {}}
28 usb_list = {'last_updated': current_datetime(), 'devices': {}}
29
30 # Generate and write keyboard specific JSON files
31 for keyboard_name in list_keyboards():
32 kb_all['keyboards'][keyboard_name] = info_json(keyboard_name)
33 keyboard_dir = v1_dir / 'keyboards' / keyboard_name
34 keyboard_info = keyboard_dir / 'info.json'
35 keyboard_readme = keyboard_dir / 'readme.md'
36 keyboard_readme_src = Path('keyboards') / keyboard_name / 'readme.md'
37
38 keyboard_dir.mkdir(parents=True, exist_ok=True)
39 keyboard_info.write_text(json.dumps(kb_all['keyboards'][keyboard_name]))
40
41 if keyboard_readme_src.exists():
42 copyfile(keyboard_readme_src, keyboard_readme)
43
44 if 'usb' in kb_all['keyboards'][keyboard_name]:
45 usb = kb_all['keyboards'][keyboard_name]['usb']
46
47 if usb['vid'] not in usb_list['devices']:
48 usb_list['devices'][usb['vid']] = {}
49
50 if usb['pid'] not in usb_list['devices'][usb['vid']]:
51 usb_list['devices'][usb['vid']][usb['pid']] = {}
52
53 usb_list['devices'][usb['vid']][usb['pid']][keyboard_name] = usb
54
55 # Write the global JSON files
56 keyboard_list.write_text(json.dumps({'last_updated': current_datetime(), 'keyboards': sorted(kb_all['keyboards'])}))
57 keyboard_all.write_text(json.dumps(kb_all))
58 usb_file.write_text(json.dumps(usb_list))
diff --git a/lib/python/qmk/cli/info.py b/lib/python/qmk/cli/info.py
index 0e64d4074..44ce1186a 100755
--- a/lib/python/qmk/cli/info.py
+++ b/lib/python/qmk/cli/info.py
@@ -16,7 +16,7 @@ ROW_LETTERS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop'
16COL_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijilmnopqrstuvwxyz' 16COL_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijilmnopqrstuvwxyz'
17 17
18 18
19def show_keymap(info_json, title_caps=True): 19def show_keymap(kb_info_json, title_caps=True):
20 """Render the keymap in ascii art. 20 """Render the keymap in ascii art.
21 """ 21 """
22 keymap_path = locate_keymap(cli.config.info.keyboard, cli.config.info.keymap) 22 keymap_path = locate_keymap(cli.config.info.keyboard, cli.config.info.keymap)
@@ -36,7 +36,7 @@ def show_keymap(info_json, title_caps=True):
36 else: 36 else:
37 cli.echo('{fg_cyan}layer_%s{fg_reset}:', layer_num) 37 cli.echo('{fg_cyan}layer_%s{fg_reset}:', layer_num)
38 38
39 print(render_layout(info_json['layouts'][layout_name]['layout'], layer)) 39 print(render_layout(kb_info_json['layouts'][layout_name]['layout'], layer))
40 40
41 41
42def show_layouts(kb_info_json, title_caps=True): 42def show_layouts(kb_info_json, title_caps=True):
@@ -48,10 +48,10 @@ def show_layouts(kb_info_json, title_caps=True):
48 print(layout_art) # Avoid passing dirty data to cli.echo() 48 print(layout_art) # Avoid passing dirty data to cli.echo()
49 49
50 50
51def show_matrix(info_json, title_caps=True): 51def show_matrix(kb_info_json, title_caps=True):
52 """Render the layout with matrix labels in ascii art. 52 """Render the layout with matrix labels in ascii art.
53 """ 53 """
54 for layout_name, layout in info_json['layouts'].items(): 54 for layout_name, layout in kb_info_json['layouts'].items():
55 # Build our label list 55 # Build our label list
56 labels = [] 56 labels = []
57 for key in layout['layout']: 57 for key in layout['layout']:
@@ -69,54 +69,54 @@ def show_matrix(info_json, title_caps=True):
69 else: 69 else:
70 cli.echo('{fg_blue}matrix_%s{fg_reset}:', layout_name) 70 cli.echo('{fg_blue}matrix_%s{fg_reset}:', layout_name)
71 71
72 print(render_layout(info_json['layouts'][layout_name]['layout'], labels)) 72 print(render_layout(kb_info_json['layouts'][layout_name]['layout'], labels))
73 73
74 74
75def print_friendly_output(info_json): 75def print_friendly_output(kb_info_json):
76 """Print the info.json in a friendly text format. 76 """Print the info.json in a friendly text format.
77 """ 77 """
78 cli.echo('{fg_blue}Keyboard Name{fg_reset}: %s', info_json.get('keyboard_name', 'Unknown')) 78 cli.echo('{fg_blue}Keyboard Name{fg_reset}: %s', kb_info_json.get('keyboard_name', 'Unknown'))
79 cli.echo('{fg_blue}Manufacturer{fg_reset}: %s', info_json.get('manufacturer', 'Unknown')) 79 cli.echo('{fg_blue}Manufacturer{fg_reset}: %s', kb_info_json.get('manufacturer', 'Unknown'))
80 if 'url' in info_json: 80 if 'url' in kb_info_json:
81 cli.echo('{fg_blue}Website{fg_reset}: %s', info_json.get('url', '')) 81 cli.echo('{fg_blue}Website{fg_reset}: %s', kb_info_json.get('url', ''))
82 if info_json.get('maintainer', 'qmk') == 'qmk': 82 if kb_info_json.get('maintainer', 'qmk') == 'qmk':
83 cli.echo('{fg_blue}Maintainer{fg_reset}: QMK Community') 83 cli.echo('{fg_blue}Maintainer{fg_reset}: QMK Community')
84 else: 84 else:
85 cli.echo('{fg_blue}Maintainer{fg_reset}: %s', info_json['maintainer']) 85 cli.echo('{fg_blue}Maintainer{fg_reset}: %s', kb_info_json['maintainer'])
86 cli.echo('{fg_blue}Keyboard Folder{fg_reset}: %s', info_json.get('keyboard_folder', 'Unknown')) 86 cli.echo('{fg_blue}Keyboard Folder{fg_reset}: %s', kb_info_json.get('keyboard_folder', 'Unknown'))
87 cli.echo('{fg_blue}Layouts{fg_reset}: %s', ', '.join(sorted(info_json['layouts'].keys()))) 87 cli.echo('{fg_blue}Layouts{fg_reset}: %s', ', '.join(sorted(kb_info_json['layouts'].keys())))
88 if 'width' in info_json and 'height' in info_json: 88 if 'width' in kb_info_json and 'height' in kb_info_json:
89 cli.echo('{fg_blue}Size{fg_reset}: %s x %s' % (info_json['width'], info_json['height'])) 89 cli.echo('{fg_blue}Size{fg_reset}: %s x %s' % (kb_info_json['width'], kb_info_json['height']))
90 cli.echo('{fg_blue}Processor{fg_reset}: %s', info_json.get('processor', 'Unknown')) 90 cli.echo('{fg_blue}Processor{fg_reset}: %s', kb_info_json.get('processor', 'Unknown'))
91 cli.echo('{fg_blue}Bootloader{fg_reset}: %s', info_json.get('bootloader', 'Unknown')) 91 cli.echo('{fg_blue}Bootloader{fg_reset}: %s', kb_info_json.get('bootloader', 'Unknown'))
92 92
93 if cli.config.info.layouts: 93 if cli.config.info.layouts:
94 show_layouts(info_json, True) 94 show_layouts(kb_info_json, True)
95 95
96 if cli.config.info.matrix: 96 if cli.config.info.matrix:
97 show_matrix(info_json, True) 97 show_matrix(kb_info_json, True)
98 98
99 if cli.config_source.info.keymap and cli.config_source.info.keymap != 'config_file': 99 if cli.config_source.info.keymap and cli.config_source.info.keymap != 'config_file':
100 show_keymap(info_json, True) 100 show_keymap(kb_info_json, True)
101 101
102 102
103def print_text_output(info_json): 103def print_text_output(kb_info_json):
104 """Print the info.json in a plain text format. 104 """Print the info.json in a plain text format.
105 """ 105 """
106 for key in sorted(info_json): 106 for key in sorted(kb_info_json):
107 if key == 'layouts': 107 if key == 'layouts':
108 cli.echo('{fg_blue}layouts{fg_reset}: %s', ', '.join(sorted(info_json['layouts'].keys()))) 108 cli.echo('{fg_blue}layouts{fg_reset}: %s', ', '.join(sorted(kb_info_json['layouts'].keys())))
109 else: 109 else:
110 cli.echo('{fg_blue}%s{fg_reset}: %s', key, info_json[key]) 110 cli.echo('{fg_blue}%s{fg_reset}: %s', key, kb_info_json[key])
111 111
112 if cli.config.info.layouts: 112 if cli.config.info.layouts:
113 show_layouts(info_json, False) 113 show_layouts(kb_info_json, False)
114 114
115 if cli.config.info.matrix: 115 if cli.config.info.matrix:
116 show_matrix(info_json, False) 116 show_matrix(kb_info_json, False)
117 117
118 if cli.config_source.info.keymap and cli.config_source.info.keymap != 'config_file': 118 if cli.config_source.info.keymap and cli.config_source.info.keymap != 'config_file':
119 show_keymap(info_json, False) 119 show_keymap(kb_info_json, False)
120 120
121 121
122@cli.argument('-kb', '--keyboard', help='Keyboard to show info for.') 122@cli.argument('-kb', '--keyboard', help='Keyboard to show info for.')
diff --git a/lib/python/qmk/cli/json2c.py b/lib/python/qmk/cli/json2c.py
index 2a9009436..426078063 100755
--- a/lib/python/qmk/cli/json2c.py
+++ b/lib/python/qmk/cli/json2c.py
@@ -38,7 +38,7 @@ def json2c(cli):
38 user_keymap = json.load(fd) 38 user_keymap = json.load(fd)
39 39
40 # Generate the keymap 40 # Generate the keymap
41 keymap_c = qmk.keymap.generate(user_keymap['keyboard'], user_keymap['layout'], user_keymap['layers']) 41 keymap_c = qmk.keymap.generate_c(user_keymap['keyboard'], user_keymap['layout'], user_keymap['layers'])
42 42
43 if cli.args.output: 43 if cli.args.output:
44 cli.args.output.parent.mkdir(parents=True, exist_ok=True) 44 cli.args.output.parent.mkdir(parents=True, exist_ok=True)
diff --git a/lib/python/qmk/cli/list/keyboards.py b/lib/python/qmk/cli/list/keyboards.py
index ca0c5661a..8b6c45167 100644
--- a/lib/python/qmk/cli/list/keyboards.py
+++ b/lib/python/qmk/cli/list/keyboards.py
@@ -1,28 +1,13 @@
1"""List the keyboards currently defined within QMK 1"""List the keyboards currently defined within QMK
2""" 2"""
3# We avoid pathlib here because this is performance critical code.
4import os
5import glob
6
7from milc import cli 3from milc import cli
8 4
9BASE_PATH = os.path.join(os.getcwd(), "keyboards") + os.path.sep 5import qmk.keyboard
10KB_WILDCARD = os.path.join(BASE_PATH, "**", "rules.mk")
11
12
13def find_name(path):
14 """Determine the keyboard name by stripping off the base_path and rules.mk.
15 """
16 return path.replace(BASE_PATH, "").replace(os.path.sep + "rules.mk", "")
17 6
18 7
19@cli.subcommand("List the keyboards currently defined within QMK") 8@cli.subcommand("List the keyboards currently defined within QMK")
20def list_keyboards(cli): 9def list_keyboards(cli):
21 """List the keyboards currently defined within QMK 10 """List the keyboards currently defined within QMK
22 """ 11 """
23 # find everywhere we have rules.mk where keymaps isn't in the path 12 for keyboard_name in qmk.keyboard.list_keyboards():
24 paths = [path for path in glob.iglob(KB_WILDCARD, recursive=True) if 'keymaps' not in path]
25
26 # Extract the keyboard name from the path and print it
27 for keyboard_name in sorted(map(find_name, paths)):
28 print(keyboard_name) 13 print(keyboard_name)
diff --git a/lib/python/qmk/constants.py b/lib/python/qmk/constants.py
index 102111d7c..94ab68e5e 100644
--- a/lib/python/qmk/constants.py
+++ b/lib/python/qmk/constants.py
@@ -12,3 +12,8 @@ MAX_KEYBOARD_SUBFOLDERS = 5
12CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F411' 12CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F411'
13LUFA_PROCESSORS = 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None 13LUFA_PROCESSORS = 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None
14VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85' 14VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85'
15
16# Common format strings
17DATE_FORMAT = '%Y-%m-%d'
18DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S %Z'
19TIME_FORMAT = '%H:%M:%S'
diff --git a/lib/python/qmk/datetime.py b/lib/python/qmk/datetime.py
new file mode 100644
index 000000000..4bffcc621
--- /dev/null
+++ b/lib/python/qmk/datetime.py
@@ -0,0 +1,29 @@
1"""Functions to work with dates and times in a uniform way.
2
3The 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.
4"""
5from time import gmtime, strftime
6
7from qmk.constants import DATE_FORMAT, DATETIME_FORMAT, TIME_FORMAT
8from qmk.decorators import lru_cache
9
10
11@lru_cache(timeout=5)
12def current_date():
13 """Returns the current time in UTZ as a formatted string.
14 """
15 return strftime(DATE_FORMAT, gmtime())
16
17
18@lru_cache(timeout=5)
19def current_datetime():
20 """Returns the current time in UTZ as a formatted string.
21 """
22 return strftime(DATETIME_FORMAT, gmtime())
23
24
25@lru_cache(timeout=5)
26def current_time():
27 """Returns the current time in UTZ as a formatted string.
28 """
29 return strftime(TIME_FORMAT, gmtime())
diff --git a/lib/python/qmk/decorators.py b/lib/python/qmk/decorators.py
index f8f2facb1..629402b09 100644
--- a/lib/python/qmk/decorators.py
+++ b/lib/python/qmk/decorators.py
@@ -2,6 +2,7 @@
2""" 2"""
3import functools 3import functools
4from pathlib import Path 4from pathlib import Path
5from time import monotonic
5 6
6from milc import cli 7from milc import cli
7 8
@@ -84,3 +85,38 @@ def automagic_keymap(func):
84 return func(*args, **kwargs) 85 return func(*args, **kwargs)
85 86
86 return wrapper 87 return wrapper
88
89
90def lru_cache(timeout=10, maxsize=128, typed=False):
91 """Least Recently Used Cache- cache the result of a function.
92
93 Args:
94
95 timeout
96 How many seconds to cache results for.
97
98 maxsize
99 The maximum size of the cache in bytes
100
101 typed
102 When `True` argument types will be taken into consideration, for example `3` and `3.0` will be treated as different keys.
103 """
104 def wrapper_cache(func):
105 func = functools.lru_cache(maxsize=maxsize, typed=typed)(func)
106 func.expiration = monotonic() + timeout
107
108 @functools.wraps(func)
109 def wrapped_func(*args, **kwargs):
110 if monotonic() >= func.expiration:
111 func.expiration = monotonic() + timeout
112
113 func.cache_clear()
114
115 return func(*args, **kwargs)
116
117 wrapped_func.cache_info = func.cache_info
118 wrapped_func.cache_clear = func.cache_clear
119
120 return wrapped_func
121
122 return wrapper_cache
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index 0e540c00a..e92c3335b 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -9,6 +9,7 @@ from milc import cli
9from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS 9from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS
10from qmk.c_parse import find_layouts 10from qmk.c_parse import find_layouts
11from qmk.keyboard import config_h, rules_mk 11from qmk.keyboard import config_h, rules_mk
12from qmk.keymap import list_keymaps
12from qmk.makefile import parse_rules_mk_file 13from qmk.makefile import parse_rules_mk_file
13from qmk.math import compute 14from qmk.math import compute
14 15
@@ -25,14 +26,21 @@ def info_json(keyboard):
25 info_data = { 26 info_data = {
26 'keyboard_name': str(keyboard), 27 'keyboard_name': str(keyboard),
27 'keyboard_folder': str(keyboard), 28 'keyboard_folder': str(keyboard),
29 'keymaps': {},
28 'layouts': {}, 30 'layouts': {},
29 'maintainer': 'qmk', 31 'maintainer': 'qmk',
30 } 32 }
31 33
34 # Populate the list of JSON keymaps
35 for keymap in list_keymaps(keyboard, c=False, fullpath=True):
36 info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'}
37
38 # Populate layout data
32 for layout_name, layout_json in _find_all_layouts(keyboard, rules).items(): 39 for layout_name, layout_json in _find_all_layouts(keyboard, rules).items():
33 if not layout_name.startswith('LAYOUT_kc'): 40 if not layout_name.startswith('LAYOUT_kc'):
34 info_data['layouts'][layout_name] = layout_json 41 info_data['layouts'][layout_name] = layout_json
35 42
43 # Merge in the data from info.json, config.h, and rules.mk
36 info_data = merge_info_jsons(keyboard, info_data) 44 info_data = merge_info_jsons(keyboard, info_data)
37 info_data = _extract_config_h(info_data) 45 info_data = _extract_config_h(info_data)
38 info_data = _extract_rules_mk(info_data) 46 info_data = _extract_rules_mk(info_data)
diff --git a/lib/python/qmk/keyboard.py b/lib/python/qmk/keyboard.py
index d1f2a301d..9ebb2d77d 100644
--- a/lib/python/qmk/keyboard.py
+++ b/lib/python/qmk/keyboard.py
@@ -3,10 +3,30 @@
3from array import array 3from array import array
4from math import ceil 4from math import ceil
5from pathlib import Path 5from pathlib import Path
6import os
7from glob import glob
6 8
7from qmk.c_parse import parse_config_h_file 9from qmk.c_parse import parse_config_h_file
8from qmk.makefile import parse_rules_mk_file 10from qmk.makefile import parse_rules_mk_file
9 11
12base_path = os.path.join(os.getcwd(), "keyboards") + os.path.sep
13
14
15def _find_name(path):
16 """Determine the keyboard name by stripping off the base_path and rules.mk.
17 """
18 return path.replace(base_path, "").replace(os.path.sep + "rules.mk", "")
19
20
21def list_keyboards():
22 """Returns a list of all keyboards.
23 """
24 # We avoid pathlib here because this is performance critical code.
25 kb_wildcard = os.path.join(base_path, "**", "rules.mk")
26 paths = [path for path in glob(kb_wildcard, recursive=True) if 'keymaps' not in path]
27
28 return sorted(map(_find_name, paths))
29
10 30
11def config_h(keyboard): 31def config_h(keyboard):
12 """Parses all the config.h files for a keyboard. 32 """Parses all the config.h files for a keyboard.
diff --git a/lib/python/qmk/keymap.py b/lib/python/qmk/keymap.py
index 166697ee6..31c61ae6a 100644
--- a/lib/python/qmk/keymap.py
+++ b/lib/python/qmk/keymap.py
@@ -29,33 +29,37 @@ __KEYMAP_GOES_HERE__
29""" 29"""
30 30
31 31
32def template(keyboard, type='c'): 32def template_json(keyboard):
33 """Returns the `keymap.c` or `keymap.json` template for a keyboard. 33 """Returns a `keymap.json` template for a keyboard.
34 34
35 If a template exists in `keyboards/<keyboard>/templates/keymap.c` that 35 If a template exists in `keyboards/<keyboard>/templates/keymap.json` that text will be used instead of an empty dictionary.
36 text will be used instead of `DEFAULT_KEYMAP_C`.
37
38 If a template exists in `keyboards/<keyboard>/templates/keymap.json` that
39 text will be used instead of an empty dictionary.
40 36
41 Args: 37 Args:
42 keyboard 38 keyboard
43 The keyboard to return a template for. 39 The keyboard to return a template for.
40 """
41 template_file = Path('keyboards/%s/templates/keymap.json' % keyboard)
42 template = {'keyboard': keyboard}
43 if template_file.exists():
44 template.update(json.loads(template_file.read_text()))
45
46 return template
47
44 48
45 type 49def template_c(keyboard):
46 'json' for `keymap.json` and 'c' (or anything else) for `keymap.c` 50 """Returns a `keymap.c` template for a keyboard.
51
52 If a template exists in `keyboards/<keyboard>/templates/keymap.c` that text will be used instead of an empty dictionary.
53
54 Args:
55 keyboard
56 The keyboard to return a template for.
47 """ 57 """
48 if type == 'json': 58 template_file = Path('keyboards/%s/templates/keymap.c' % keyboard)
49 template_file = Path('keyboards/%s/templates/keymap.json' % keyboard) 59 if template_file.exists():
50 template = {'keyboard': keyboard} 60 template = template_file.read_text()
51 if template_file.exists():
52 template.update(json.loads(template_file.read_text()))
53 else: 61 else:
54 template_file = Path('keyboards/%s/templates/keymap.c' % keyboard) 62 template = DEFAULT_KEYMAP_C
55 if template_file.exists():
56 template = template_file.read_text()
57 else:
58 template = DEFAULT_KEYMAP_C
59 63
60 return template 64 return template
61 65
@@ -69,15 +73,65 @@ def _strip_any(keycode):
69 return keycode 73 return keycode
70 74
71 75
72def is_keymap_dir(keymap): 76def is_keymap_dir(keymap, c=True, json=True, additional_files=None):
73 """Return True if Path object `keymap` has a keymap file inside. 77 """Return True if Path object `keymap` has a keymap file inside.
78
79 Args:
80 keymap
81 A Path() object for the keymap directory you want to check.
82
83 c
84 When true include `keymap.c` keymaps.
85
86 json
87 When true include `keymap.json` keymaps.
88
89 additional_files
90 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'])`
74 """ 91 """
75 for file in ('keymap.c', 'keymap.json'): 92 files = []
93
94 if c:
95 files.append('keymap.c')
96
97 if json:
98 files.append('keymap.json')
99
100 for file in files:
76 if (keymap / file).is_file(): 101 if (keymap / file).is_file():
102 if additional_files:
103 for file in additional_files:
104 if not (keymap / file).is_file():
105 return False
106
77 return True 107 return True
78 108
79 109
80def generate(keyboard, layout, layers, type='c', keymap=None): 110def generate_json(keymap, keyboard, layout, layers):
111 """Returns a `keymap.json` for the specified keyboard, layout, and layers.
112
113 Args:
114 keymap
115 A name for this keymap.
116
117 keyboard
118 The name of the keyboard.
119
120 layout
121 The LAYOUT macro this keymap uses.
122
123 layers
124 An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode.
125 """
126 new_keymap = template_json(keyboard)
127 new_keymap['keymap'] = keymap
128 new_keymap['layout'] = layout
129 new_keymap['layers'] = layers
130
131 return new_keymap
132
133
134def generate_c(keyboard, layout, layers):
81 """Returns a `keymap.c` or `keymap.json` for the specified keyboard, layout, and layers. 135 """Returns a `keymap.c` or `keymap.json` for the specified keyboard, layout, and layers.
82 136
83 Args: 137 Args:
@@ -89,33 +143,33 @@ def generate(keyboard, layout, layers, type='c', keymap=None):
89 143
90 layers 144 layers
91 An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode. 145 An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode.
92
93 type
94 'json' for `keymap.json` and 'c' (or anything else) for `keymap.c`
95 """ 146 """
96 new_keymap = template(keyboard, type) 147 new_keymap = template_c(keyboard)
97 if type == 'json': 148 layer_txt = []
98 new_keymap['keymap'] = keymap 149 for layer_num, layer in enumerate(layers):
99 new_keymap['layout'] = layout 150 if layer_num != 0:
100 new_keymap['layers'] = layers 151 layer_txt[-1] = layer_txt[-1] + ','
101 else: 152 layer = map(_strip_any, layer)
102 layer_txt = [] 153 layer_keys = ', '.join(layer)
103 for layer_num, layer in enumerate(layers): 154 layer_txt.append('\t[%s] = %s(%s)' % (layer_num, layout, layer_keys))
104 if layer_num != 0: 155
105 layer_txt[-1] = layer_txt[-1] + ',' 156 keymap = '\n'.join(layer_txt)
157 new_keymap = new_keymap.replace('__KEYMAP_GOES_HERE__', keymap)
106 158
107 layer = map(_strip_any, layer) 159 return new_keymap
108 layer_keys = ', '.join(layer)
109 layer_txt.append('\t[%s] = %s(%s)' % (layer_num, layout, layer_keys))
110 160
111 keymap = '\n'.join(layer_txt)
112 new_keymap = new_keymap.replace('__KEYMAP_GOES_HERE__', keymap)
113 161
114 return new_keymap 162def write_file(keymap_filename, keymap_content):
163 keymap_filename.parent.mkdir(parents=True, exist_ok=True)
164 keymap_filename.write_text(keymap_content)
115 165
166 cli.log.info('Wrote keymap to {fg_cyan}%s', keymap_filename)
167
168 return keymap_filename
116 169
117def write(keyboard, keymap, layout, layers, type='c'): 170
118 """Generate the `keymap.c` and write it to disk. 171def write_json(keyboard, keymap, layout, layers):
172 """Generate the `keymap.json` and write it to disk.
119 173
120 Returns the filename written to. 174 Returns the filename written to.
121 175
@@ -131,23 +185,36 @@ def write(keyboard, keymap, layout, layers, type='c'):
131 185
132 layers 186 layers
133 An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode. 187 An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode.
134
135 type
136 'json' for `keymap.json` and 'c' (or anything else) for `keymap.c`
137 """ 188 """
138 keymap_content = generate(keyboard, layout, layers, type) 189 keymap_json = generate_json(keyboard, keymap, layout, layers)
139 if type == 'json': 190 keymap_content = json.dumps(keymap_json)
140 keymap_file = qmk.path.keymap(keyboard) / keymap / 'keymap.json' 191 keymap_file = qmk.path.keymap(keyboard) / keymap / 'keymap.json'
141 keymap_content = json.dumps(keymap_content) 192
142 else: 193 return write_file(keymap_file, keymap_content)
143 keymap_file = qmk.path.keymap(keyboard) / keymap / 'keymap.c' 194
195
196def write(keyboard, keymap, layout, layers):
197 """Generate the `keymap.c` and write it to disk.
198
199 Returns the filename written to.
200
201 Args:
202 keyboard
203 The name of the keyboard
144 204
145 keymap_file.parent.mkdir(parents=True, exist_ok=True) 205 keymap
146 keymap_file.write_text(keymap_content) 206 The name of the keymap
147 207
148 cli.log.info('Wrote keymap to {fg_cyan}%s', keymap_file) 208 layout
209 The LAYOUT macro this keymap uses.
210
211 layers
212 An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode.
213 """
214 keymap_content = generate_c(keyboard, layout, layers)
215 keymap_file = qmk.path.keymap(keyboard) / keymap / 'keymap.c'
149 216
150 return keymap_file 217 return write_file(keymap_file, keymap_content)
151 218
152 219
153def locate_keymap(keyboard, keymap): 220def locate_keymap(keyboard, keymap):
@@ -189,38 +256,58 @@ def locate_keymap(keyboard, keymap):
189 return community_layout / 'keymap.c' 256 return community_layout / 'keymap.c'
190 257
191 258
192def list_keymaps(keyboard): 259def list_keymaps(keyboard, c=True, json=True, additional_files=None, fullpath=False):
193 """ List the available keymaps for a keyboard. 260 """List the available keymaps for a keyboard.
194 261
195 Args: 262 Args:
196 keyboard: the keyboards full name with vendor and revision if necessary, example: clueboard/66/rev3 263 keyboard
264 The keyboards full name with vendor and revision if necessary, example: clueboard/66/rev3
265
266 c
267 When true include `keymap.c` keymaps.
268
269 json
270 When true include `keymap.json` keymaps.
271
272 additional_files
273 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'])`
274
275 fullpath
276 When set to True the full path of the keymap relative to the `qmk_firmware` root will be provided.
197 277
198 Returns: 278 Returns:
199 a set with the names of the available keymaps 279 a sorted list of valid keymap names.
200 """ 280 """
201 # parse all the rules.mk files for the keyboard 281 # parse all the rules.mk files for the keyboard
202 rules = rules_mk(keyboard) 282 rules = rules_mk(keyboard)
203 names = set() 283 names = set()
204 284
205 if rules: 285 if rules:
206 # qmk_firmware/keyboards
207 keyboards_dir = Path('keyboards') 286 keyboards_dir = Path('keyboards')
208 # path to the keyboard's directory
209 kb_path = keyboards_dir / keyboard 287 kb_path = keyboards_dir / keyboard
288
210 # walk up the directory tree until keyboards_dir 289 # walk up the directory tree until keyboards_dir
211 # and collect all directories' name with keymap.c file in it 290 # and collect all directories' name with keymap.c file in it
212 while kb_path != keyboards_dir: 291 while kb_path != keyboards_dir:
213 keymaps_dir = kb_path / "keymaps" 292 keymaps_dir = kb_path / "keymaps"
214 if keymaps_dir.exists(): 293
215 names = names.union([keymap.name for keymap in keymaps_dir.iterdir() if is_keymap_dir(keymap)]) 294 if keymaps_dir.is_dir():
295 for keymap in keymaps_dir.iterdir():
296 if is_keymap_dir(keymap, c, json, additional_files):
297 keymap = keymap if fullpath else keymap.name
298 names.add(keymap)
299
216 kb_path = kb_path.parent 300 kb_path = kb_path.parent
217 301
218 # if community layouts are supported, get them 302 # if community layouts are supported, get them
219 if "LAYOUTS" in rules: 303 if "LAYOUTS" in rules:
220 for layout in rules["LAYOUTS"].split(): 304 for layout in rules["LAYOUTS"].split():
221 cl_path = Path('layouts/community') / layout 305 cl_path = Path('layouts/community') / layout
222 if cl_path.exists(): 306 if cl_path.is_dir():
223 names = names.union([keymap.name for keymap in cl_path.iterdir() if is_keymap_dir(keymap)]) 307 for keymap in cl_path.iterdir():
308 if is_keymap_dir(keymap, c, json, additional_files):
309 keymap = keymap if fullpath else keymap.name
310 names.add(keymap)
224 311
225 return sorted(names) 312 return sorted(names)
226 313
diff --git a/lib/python/qmk/tests/test_qmk_keymap.py b/lib/python/qmk/tests/test_qmk_keymap.py
index 7ef708e0d..f1ecf2937 100644
--- a/lib/python/qmk/tests/test_qmk_keymap.py
+++ b/lib/python/qmk/tests/test_qmk_keymap.py
@@ -1,33 +1,33 @@
1import qmk.keymap 1import qmk.keymap
2 2
3 3
4def test_template_onekey_proton_c(): 4def test_template_c_onekey_proton_c():
5 templ = qmk.keymap.template('handwired/onekey/proton_c') 5 templ = qmk.keymap.template_c('handwired/onekey/proton_c')
6 assert templ == qmk.keymap.DEFAULT_KEYMAP_C 6 assert templ == qmk.keymap.DEFAULT_KEYMAP_C
7 7
8 8
9def test_template_onekey_proton_c_json(): 9def test_template_json_onekey_proton_c():
10 templ = qmk.keymap.template('handwired/onekey/proton_c', type='json') 10 templ = qmk.keymap.template_json('handwired/onekey/proton_c')
11 assert templ == {'keyboard': 'handwired/onekey/proton_c'} 11 assert templ == {'keyboard': 'handwired/onekey/proton_c'}
12 12
13 13
14def test_template_onekey_pytest(): 14def test_template_c_onekey_pytest():
15 templ = qmk.keymap.template('handwired/onekey/pytest') 15 templ = qmk.keymap.template_c('handwired/onekey/pytest')
16 assert templ == '#include QMK_KEYBOARD_H\nconst uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {__KEYMAP_GOES_HERE__};\n' 16 assert templ == '#include QMK_KEYBOARD_H\nconst uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {__KEYMAP_GOES_HERE__};\n'
17 17
18 18
19def test_template_onekey_pytest_json(): 19def test_template_json_onekey_pytest():
20 templ = qmk.keymap.template('handwired/onekey/pytest', type='json') 20 templ = qmk.keymap.template_json('handwired/onekey/pytest')
21 assert templ == {'keyboard': 'handwired/onekey/pytest', "documentation": "This file is a keymap.json file for handwired/onekey/pytest"} 21 assert templ == {'keyboard': 'handwired/onekey/pytest', "documentation": "This file is a keymap.json file for handwired/onekey/pytest"}
22 22
23 23
24def test_generate_onekey_pytest(): 24def test_generate_c_onekey_pytest():
25 templ = qmk.keymap.generate('handwired/onekey/pytest', 'LAYOUT', [['KC_A']]) 25 templ = qmk.keymap.generate_c('handwired/onekey/pytest', 'LAYOUT', [['KC_A']])
26 assert templ == '#include QMK_KEYBOARD_H\nconst uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {\t[0] = LAYOUT(KC_A)};\n' 26 assert templ == '#include QMK_KEYBOARD_H\nconst uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {\t[0] = LAYOUT(KC_A)};\n'
27 27
28 28
29def test_generate_onekey_pytest_json(): 29def test_generate_json_onekey_pytest():
30 templ = qmk.keymap.generate('handwired/onekey/pytest', 'LAYOUT', [['KC_A']], type='json', keymap='default') 30 templ = qmk.keymap.generate_json('default', 'handwired/onekey/pytest', 'LAYOUT', [['KC_A']])
31 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"]]} 31 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"]]}
32 32
33 33