Skip to content

Commit a25dd58

Browse files
authored
QMK CLI and JSON keymap support (qmk#6176)
* Script to generate keymap.c from JSON file. * Support for keymap.json * Add a warning about the keymap.c getting overwritten. * Fix keymap generating * Install the python deps * Flesh out more of the python environment * Remove defunct json2keymap * Style everything with yapf * Polish up python support * Hide json keymap.c into the .build dir * Polish up qmk-compile-json * Make milc work with positional arguments * Fix a couple small things * Fix some errors and make the CLI more understandable * Make the qmk wrapper more robust * Add basic QMK Doctor * Clean up docstrings and flesh them out as needed * remove unused compile_firmware() function
1 parent 7ba82cb commit a25dd58

34 files changed

+1988
-83
lines changed

.editorconfig

+4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ insert_final_newline = true
1616
trim_trailing_whitespace = false
1717
indent_size = 4
1818

19+
[{qmk,*.py}]
20+
charset = utf-8
21+
max_line_length = 200
22+
1923
# Make these match what we have in .gitattributes
2024
[*.mk]
2125
end_of_line = lf

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,6 @@ util/Win_Check_Output.txt
7070
secrets.tar
7171
id_rsa_*
7272
/.vs
73+
74+
# python things
75+
__pycache__

bin/qmk

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#!/usr/bin/env python3
2+
"""CLI wrapper for running QMK commands.
3+
"""
4+
import os
5+
import subprocess
6+
import sys
7+
from glob import glob
8+
from time import strftime
9+
from importlib import import_module
10+
from importlib.util import find_spec
11+
12+
# Add the QMK python libs to our path
13+
script_dir = os.path.dirname(os.path.realpath(__file__))
14+
qmk_dir = os.path.abspath(os.path.join(script_dir, '..'))
15+
python_lib_dir = os.path.abspath(os.path.join(qmk_dir, 'lib', 'python'))
16+
sys.path.append(python_lib_dir)
17+
18+
# Change to the root of our checkout
19+
os.environ['ORIG_CWD'] = os.getcwd()
20+
os.chdir(qmk_dir)
21+
22+
# Make sure our modules have been setup
23+
with open('requirements.txt', 'r') as fd:
24+
for line in fd.readlines():
25+
line = line.strip().replace('<', '=').replace('>', '=')
26+
27+
if line[0] == '#':
28+
continue
29+
30+
if '#' in line:
31+
line = line.split('#')[0]
32+
33+
module = line.split('=')[0] if '=' in line else line
34+
if not find_spec(module):
35+
print('Your QMK build environment is not fully setup!\n')
36+
print('Please run `./util/qmk_install.sh` to setup QMK.')
37+
exit(255)
38+
39+
# Figure out our version
40+
command = ['git', 'describe', '--abbrev=6', '--dirty', '--always', '--tags']
41+
result = subprocess.run(command, text=True, capture_output=True)
42+
43+
if result.returncode == 0:
44+
os.environ['QMK_VERSION'] = 'QMK ' + result.stdout.strip()
45+
else:
46+
os.environ['QMK_VERSION'] = 'QMK ' + strftime('%Y-%m-%d-%H:%M:%S')
47+
48+
# Setup the CLI
49+
import milc
50+
milc.EMOJI_LOGLEVELS['INFO'] = '{fg_blue}ψ{style_reset_all}'
51+
52+
# If we were invoked as `qmk <cmd>` massage sys.argv into `qmk-<cmd>`.
53+
# This means we can't accept arguments to the qmk script itself.
54+
script_name = os.path.basename(sys.argv[0])
55+
if script_name == 'qmk':
56+
if len(sys.argv) == 1:
57+
milc.cli.log.error('No subcommand specified!\n')
58+
59+
if len(sys.argv) == 1 or sys.argv[1] in ['-h', '--help']:
60+
milc.cli.echo('usage: qmk <subcommand> [...]')
61+
milc.cli.echo('\nsubcommands:')
62+
subcommands = glob(os.path.join(qmk_dir, 'bin', 'qmk-*'))
63+
for subcommand in sorted(subcommands):
64+
subcommand = os.path.basename(subcommand).split('-', 1)[1]
65+
milc.cli.echo('\t%s', subcommand)
66+
milc.cli.echo('\nqmk <subcommand> --help for more information')
67+
exit(1)
68+
69+
if sys.argv[1] in ['-V', '--version']:
70+
milc.cli.echo(os.environ['QMK_VERSION'])
71+
exit(0)
72+
73+
sys.argv[0] = script_name = '-'.join((script_name, sys.argv[1]))
74+
del sys.argv[1]
75+
76+
# Look for which module to import
77+
if script_name == 'qmk':
78+
milc.cli.print_help()
79+
exit(0)
80+
elif not script_name.startswith('qmk-'):
81+
milc.cli.log.error('Invalid symlink, must start with "qmk-": %s', script_name)
82+
else:
83+
subcommand = script_name.replace('-', '.').replace('_', '.').split('.')
84+
subcommand.insert(1, 'cli')
85+
subcommand = '.'.join(subcommand)
86+
87+
try:
88+
import_module(subcommand)
89+
except ModuleNotFoundError as e:
90+
if e.__class__.__name__ != subcommand:
91+
raise
92+
93+
milc.cli.log.error('Invalid subcommand! Could not import %s.', subcommand)
94+
exit(1)
95+
96+
if __name__ == '__main__':
97+
milc.cli()

bin/qmk-compile-json

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
qmk

bin/qmk-doctor

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
qmk

bin/qmk-hello

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
qmk

bin/qmk-json-keymap

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
qmk

build_json.mk

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Look for a json keymap file
2+
ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_5)/keymap.json)","")
3+
KEYMAP_C := $(KEYBOARD_OUTPUT)/src/keymap.c
4+
KEYMAP_JSON := $(MAIN_KEYMAP_PATH_5)/keymap.json
5+
KEYMAP_PATH := $(MAIN_KEYMAP_PATH_5)
6+
else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_4)/keymap.json)","")
7+
KEYMAP_C := $(KEYBOARD_OUTPUT)/src/keymap.c
8+
KEYMAP_JSON := $(MAIN_KEYMAP_PATH_4)/keymap.json
9+
KEYMAP_PATH := $(MAIN_KEYMAP_PATH_4)
10+
else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_3)/keymap.json)","")
11+
KEYMAP_C := $(KEYBOARD_OUTPUT)/src/keymap.c
12+
KEYMAP_JSON := $(MAIN_KEYMAP_PATH_3)/keymap.json
13+
KEYMAP_PATH := $(MAIN_KEYMAP_PATH_3)
14+
else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_2)/keymap.json)","")
15+
KEYMAP_C := $(KEYBOARD_OUTPUT)/src/keymap.c
16+
KEYMAP_JSON := $(MAIN_KEYMAP_PATH_2)/keymap.json
17+
KEYMAP_PATH := $(MAIN_KEYMAP_PATH_2)
18+
else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_1)/keymap.json)","")
19+
KEYMAP_C := $(KEYBOARD_OUTPUT)/src/keymap.c
20+
KEYMAP_JSON := $(MAIN_KEYMAP_PATH_1)/keymap.json
21+
KEYMAP_PATH := $(MAIN_KEYMAP_PATH_1)
22+
endif
23+
24+
# Generate the keymap.c
25+
ifneq ("$(KEYMAP_JSON)","")
26+
_ = $(shell bin/qmk-json-keymap -f $(KEYMAP_JSON) -o $(KEYMAP_C))
27+
endif

build_keyboard.mk

+33-27
Original file line numberDiff line numberDiff line change
@@ -98,31 +98,38 @@ MAIN_KEYMAP_PATH_3 := $(KEYBOARD_PATH_3)/keymaps/$(KEYMAP)
9898
MAIN_KEYMAP_PATH_4 := $(KEYBOARD_PATH_4)/keymaps/$(KEYMAP)
9999
MAIN_KEYMAP_PATH_5 := $(KEYBOARD_PATH_5)/keymaps/$(KEYMAP)
100100

101-
ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_5)/keymap.c)","")
102-
-include $(MAIN_KEYMAP_PATH_5)/rules.mk
103-
KEYMAP_C := $(MAIN_KEYMAP_PATH_5)/keymap.c
104-
KEYMAP_PATH := $(MAIN_KEYMAP_PATH_5)
105-
else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_4)/keymap.c)","")
106-
-include $(MAIN_KEYMAP_PATH_4)/rules.mk
107-
KEYMAP_C := $(MAIN_KEYMAP_PATH_4)/keymap.c
108-
KEYMAP_PATH := $(MAIN_KEYMAP_PATH_4)
109-
else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_3)/keymap.c)","")
110-
-include $(MAIN_KEYMAP_PATH_3)/rules.mk
111-
KEYMAP_C := $(MAIN_KEYMAP_PATH_3)/keymap.c
112-
KEYMAP_PATH := $(MAIN_KEYMAP_PATH_3)
113-
else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_2)/keymap.c)","")
114-
-include $(MAIN_KEYMAP_PATH_2)/rules.mk
115-
KEYMAP_C := $(MAIN_KEYMAP_PATH_2)/keymap.c
116-
KEYMAP_PATH := $(MAIN_KEYMAP_PATH_2)
117-
else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_1)/keymap.c)","")
118-
-include $(MAIN_KEYMAP_PATH_1)/rules.mk
119-
KEYMAP_C := $(MAIN_KEYMAP_PATH_1)/keymap.c
120-
KEYMAP_PATH := $(MAIN_KEYMAP_PATH_1)
121-
else ifneq ($(LAYOUTS),)
122-
include build_layout.mk
123-
else
124-
$(error Could not find keymap)
125-
# this state should never be reached
101+
# Check for keymap.json first, so we can regenerate keymap.c
102+
include build_json.mk
103+
104+
ifeq ("$(wildcard $(KEYMAP_PATH))", "")
105+
# Look through the possible keymap folders until we find a matching keymap.c
106+
ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_5)/keymap.c)","")
107+
-include $(MAIN_KEYMAP_PATH_5)/rules.mk
108+
KEYMAP_C := $(MAIN_KEYMAP_PATH_5)/keymap.c
109+
KEYMAP_PATH := $(MAIN_KEYMAP_PATH_5)
110+
else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_4)/keymap.c)","")
111+
-include $(MAIN_KEYMAP_PATH_4)/rules.mk
112+
KEYMAP_C := $(MAIN_KEYMAP_PATH_4)/keymap.c
113+
KEYMAP_PATH := $(MAIN_KEYMAP_PATH_4)
114+
else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_3)/keymap.c)","")
115+
-include $(MAIN_KEYMAP_PATH_3)/rules.mk
116+
KEYMAP_C := $(MAIN_KEYMAP_PATH_3)/keymap.c
117+
KEYMAP_PATH := $(MAIN_KEYMAP_PATH_3)
118+
else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_2)/keymap.c)","")
119+
-include $(MAIN_KEYMAP_PATH_2)/rules.mk
120+
KEYMAP_C := $(MAIN_KEYMAP_PATH_2)/keymap.c
121+
KEYMAP_PATH := $(MAIN_KEYMAP_PATH_2)
122+
else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_1)/keymap.c)","")
123+
-include $(MAIN_KEYMAP_PATH_1)/rules.mk
124+
KEYMAP_C := $(MAIN_KEYMAP_PATH_1)/keymap.c
125+
KEYMAP_PATH := $(MAIN_KEYMAP_PATH_1)
126+
else ifneq ($(LAYOUTS),)
127+
# If we haven't found a keymap yet fall back to community layouts
128+
include build_layout.mk
129+
else
130+
$(error Could not find keymap)
131+
# this state should never be reached
132+
endif
126133
endif
127134

128135
ifeq ($(strip $(CTPC)), yes)
@@ -313,7 +320,6 @@ ifneq ("$(wildcard $(USER_PATH)/config.h)","")
313320
CONFIG_H += $(USER_PATH)/config.h
314321
endif
315322

316-
317323
# Object files directory
318324
# To put object files in current directory, use a dot (.), do NOT make
319325
# this an empty or blank macro!
@@ -323,7 +329,7 @@ ifneq ("$(wildcard $(KEYMAP_PATH)/config.h)","")
323329
CONFIG_H += $(KEYMAP_PATH)/config.h
324330
endif
325331

326-
# # project specific files
332+
# project specific files
327333
SRC += $(KEYBOARD_SRC) \
328334
$(KEYMAP_C) \
329335
$(QUANTUM_SRC)

docs/_summary.md

+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
* [QMK Basics](README.md)
1010
* [QMK Introduction](getting_started_introduction.md)
11+
* [QMK CLI](cli.md)
1112
* [Contributing to QMK](contributing.md)
1213
* [How to Use Github](getting_started_github.md)
1314
* [Getting Help](getting_started_getting_help.md)
@@ -34,13 +35,16 @@
3435
* [Keyboard Guidelines](hardware_keyboard_guidelines.md)
3536
* [Config Options](config_options.md)
3637
* [Keycodes](keycodes.md)
38+
* [Coding Conventions - C](coding_conventions_c.md)
39+
* [Coding Conventions - Python](coding_conventions_python.md)
3740
* [Documentation Best Practices](documentation_best_practices.md)
3841
* [Documentation Templates](documentation_templates.md)
3942
* [Glossary](reference_glossary.md)
4043
* [Unit Testing](unit_testing.md)
4144
* [Useful Functions](ref_functions.md)
4245
* [Configurator Support](reference_configurator_support.md)
4346
* [info.json Format](reference_info_json.md)
47+
* [Python Development](python_development.md)
4448

4549
* [Features](features.md)
4650
* [Basic Keycodes](keycodes_basic.md)

docs/cli.md

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# QMK CLI
2+
3+
This page describes how to setup and use the QMK CLI.
4+
5+
# Overview
6+
7+
The QMK CLI makes building and working with QMK keyboards easier. We have provided a number of commands to help you work with QMK:
8+
9+
* `qmk compile-json`
10+
11+
# Setup
12+
13+
Simply add the `qmk_firmware/bin` directory to your `PATH`. You can run the `qmk` commands from any directory.
14+
15+
```
16+
export PATH=$PATH:$HOME/qmk_firmware/bin
17+
```
18+
19+
You may want to add this to your `.profile`, `.bash_profile`, `.zsh_profile`, or other shell startup scripts.
20+
21+
# Commands
22+
23+
## `qmk compile-json`
24+
25+
This command allows you to compile JSON files you have downloaded from <https://config.qmk.fm>.
26+
27+
**Usage**:
28+
29+
```
30+
qmk compile-json mine.json
31+
```

docs/coding_conventions_c.md

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Coding Conventions (C)
2+
3+
Most of our style is pretty easy to pick up on, but right now it's not entirely consistent. You should match the style of the code surrounding your change, but if that code is inconsistent or unclear use the following guidelines:
4+
5+
* We indent using four (4) spaces (soft tabs)
6+
* We use a modified One True Brace Style
7+
* Opening Brace: At the end of the same line as the statement that opens the block
8+
* Closing Brace: Lined up with the first character of the statement that opens the block
9+
* Else If: Place the closing brace at the beginning of the line and the next opening brace at the end of the same line.
10+
* Optional Braces: Always include optional braces.
11+
* Good: if (condition) { return false; }
12+
* Bad: if (condition) return false;
13+
* We encourage use of C style comments: `/* */`
14+
* Think of them as a story describing the feature
15+
* Use them liberally to explain why particular decisions were made.
16+
* Do not write obvious comments
17+
* If you not sure if a comment is obvious, go ahead and include it.
18+
* In general we don't wrap lines, they can be as long as needed. If you do choose to wrap lines please do not wrap any wider than 76 columns.
19+
* We use `#pragma once` at the start of header files rather than old-style include guards (`#ifndef THIS_FILE_H`, `#define THIS_FILE_H`, ..., `#endif`)
20+
* We accept both forms of preprocessor if's: `#ifdef DEFINED` and `#if defined(DEFINED)`
21+
* If you are not sure which to prefer use the `#if defined(DEFINED)` form.
22+
* Do not change existing code from one style to the other, except when moving to a multiple condition `#if`.
23+
* Do not put whitespace between `#` and `if`.
24+
* When deciding how (or if) to indent directives keep these points in mind:
25+
* Readability is more important than consistency.
26+
* Follow the file's existing style. If the file is mixed follow the style that makes sense for the section you are modifying.
27+
* When choosing to indent you can follow the indention level of the surrounding C code, or preprocessor directives can have their own indent level. Choose the style that best communicates the intent of your code.
28+
29+
Here is an example for easy reference:
30+
31+
```c
32+
/* Enums for foo */
33+
enum foo_state {
34+
FOO_BAR,
35+
FOO_BAZ,
36+
};
37+
38+
/* Returns a value */
39+
int foo(void) {
40+
if (some_condition) {
41+
return FOO_BAR;
42+
} else {
43+
return -1;
44+
}
45+
}
46+
```
47+
48+
# Auto-formatting with clang-format
49+
50+
[Clang-format](https://clang.llvm.org/docs/ClangFormat.html) is part of LLVM and can automatically format your code for you, because ain't nobody got time to do it manually. We supply a configuration file for it that applies most of the coding conventions listed above. It will only change whitespace and newlines, so you will still have to remember to include optional braces yourself.
51+
52+
Use the [full LLVM installer](http://llvm.org/builds/) to get clang-format on Windows, or use `sudo apt install clang-format` on Ubuntu.
53+
54+
If you run it from the command-line, pass `-style=file` as an option and it will automatically find the .clang-format configuration file in the QMK root directory.
55+
56+
If you use VSCode, the standard C/C++ plugin supports clang-format, alternatively there is a [separate extension](https://marketplace.visualstudio.com/items?itemName=LLVMExtensions.ClangFormat) for it.
57+
58+
Some things (like LAYOUT macros) are destroyed by clang-format, so either don't run it on those files, or wrap the sensitive code in `// clang-format off` and `// clang-format on`.

0 commit comments

Comments
 (0)