aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.editorconfig4
-rw-r--r--.gitignore3
-rwxr-xr-xbin/qmk97
l---------bin/qmk-compile-json1
l---------bin/qmk-doctor1
l---------bin/qmk-hello1
l---------bin/qmk-json-keymap1
-rw-r--r--build_json.mk27
-rw-r--r--build_keyboard.mk60
-rw-r--r--docs/_summary.md4
-rw-r--r--docs/cli.md31
-rw-r--r--docs/coding_conventions_c.md58
-rw-r--r--docs/coding_conventions_python.md314
-rw-r--r--docs/contributing.md58
-rw-r--r--docs/python_development.md45
-rw-r--r--keyboards/clueboard/66_hotswap/keymaps/json/keymap.json1
-rw-r--r--lib/python/milc.py716
-rw-r--r--lib/python/qmk/__init__.py0
-rw-r--r--lib/python/qmk/cli/compile/__init__.py0
-rwxr-xr-xlib/python/qmk/cli/compile/json.py44
-rwxr-xr-xlib/python/qmk/cli/doctor.py47
-rwxr-xr-xlib/python/qmk/cli/hello.py13
-rw-r--r--lib/python/qmk/cli/json/__init__.py0
-rwxr-xr-xlib/python/qmk/cli/json/keymap.py54
-rw-r--r--lib/python/qmk/errors.py6
-rw-r--r--lib/python/qmk/keymap.py100
-rw-r--r--lib/python/qmk/path.py32
-rw-r--r--requirements.txt5
-rw-r--r--setup.cfg330
-rwxr-xr-xutil/freebsd_install.sh2
-rwxr-xr-xutil/linux_install.sh5
-rwxr-xr-xutil/macos_install.sh3
-rwxr-xr-xutil/msys2_install.sh3
-rwxr-xr-xutil/wsl_install.sh5
34 files changed, 1988 insertions, 83 deletions
diff --git a/.editorconfig b/.editorconfig
index 26e3a39cf..60827f04b 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -16,6 +16,10 @@ insert_final_newline = true
16trim_trailing_whitespace = false 16trim_trailing_whitespace = false
17indent_size = 4 17indent_size = 4
18 18
19[{qmk,*.py}]
20charset = utf-8
21max_line_length = 200
22
19# Make these match what we have in .gitattributes 23# Make these match what we have in .gitattributes
20[*.mk] 24[*.mk]
21end_of_line = lf 25end_of_line = lf
diff --git a/.gitignore b/.gitignore
index 7cd7fa801..140bf4aa7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -70,3 +70,6 @@ util/Win_Check_Output.txt
70secrets.tar 70secrets.tar
71id_rsa_* 71id_rsa_*
72/.vs 72/.vs
73
74# python things
75__pycache__
diff --git a/bin/qmk b/bin/qmk
new file mode 100755
index 000000000..c34365bed
--- /dev/null
+++ b/bin/qmk
@@ -0,0 +1,97 @@
1#!/usr/bin/env python3
2"""CLI wrapper for running QMK commands.
3"""
4import os
5import subprocess
6import sys
7from glob import glob
8from time import strftime
9from importlib import import_module
10from importlib.util import find_spec
11
12# Add the QMK python libs to our path
13script_dir = os.path.dirname(os.path.realpath(__file__))
14qmk_dir = os.path.abspath(os.path.join(script_dir, '..'))
15python_lib_dir = os.path.abspath(os.path.join(qmk_dir, 'lib', 'python'))
16sys.path.append(python_lib_dir)
17
18# Change to the root of our checkout
19os.environ['ORIG_CWD'] = os.getcwd()
20os.chdir(qmk_dir)
21
22# Make sure our modules have been setup
23with 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
40command = ['git', 'describe', '--abbrev=6', '--dirty', '--always', '--tags']
41result = subprocess.run(command, text=True, capture_output=True)
42
43if result.returncode == 0:
44 os.environ['QMK_VERSION'] = 'QMK ' + result.stdout.strip()
45else:
46 os.environ['QMK_VERSION'] = 'QMK ' + strftime('%Y-%m-%d-%H:%M:%S')
47
48# Setup the CLI
49import milc
50milc.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.
54script_name = os.path.basename(sys.argv[0])
55if 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
77if script_name == 'qmk':
78 milc.cli.print_help()
79 exit(0)
80elif not script_name.startswith('qmk-'):
81 milc.cli.log.error('Invalid symlink, must start with "qmk-": %s', script_name)
82else:
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
96if __name__ == '__main__':
97 milc.cli()
diff --git a/bin/qmk-compile-json b/bin/qmk-compile-json
new file mode 120000
index 000000000..c92dce8a1
--- /dev/null
+++ b/bin/qmk-compile-json
@@ -0,0 +1 @@
qmk \ No newline at end of file
diff --git a/bin/qmk-doctor b/bin/qmk-doctor
new file mode 120000
index 000000000..c92dce8a1
--- /dev/null
+++ b/bin/qmk-doctor
@@ -0,0 +1 @@
qmk \ No newline at end of file
diff --git a/bin/qmk-hello b/bin/qmk-hello
new file mode 120000
index 000000000..c92dce8a1
--- /dev/null
+++ b/bin/qmk-hello
@@ -0,0 +1 @@
qmk \ No newline at end of file
diff --git a/bin/qmk-json-keymap b/bin/qmk-json-keymap
new file mode 120000
index 000000000..c92dce8a1
--- /dev/null
+++ b/bin/qmk-json-keymap
@@ -0,0 +1 @@
qmk \ No newline at end of file
diff --git a/build_json.mk b/build_json.mk
new file mode 100644
index 000000000..2e23ed148
--- /dev/null
+++ b/build_json.mk
@@ -0,0 +1,27 @@
1# Look for a json keymap file
2ifneq ("$(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)
6else 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)
10else 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)
14else 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)
18else 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)
22endif
23
24# Generate the keymap.c
25ifneq ("$(KEYMAP_JSON)","")
26 _ = $(shell bin/qmk-json-keymap -f $(KEYMAP_JSON) -o $(KEYMAP_C))
27endif
diff --git a/build_keyboard.mk b/build_keyboard.mk
index 213cb4445..0e3c5ea23 100644
--- a/build_keyboard.mk
+++ b/build_keyboard.mk
@@ -98,31 +98,38 @@ MAIN_KEYMAP_PATH_3 := $(KEYBOARD_PATH_3)/keymaps/$(KEYMAP)
98MAIN_KEYMAP_PATH_4 := $(KEYBOARD_PATH_4)/keymaps/$(KEYMAP) 98MAIN_KEYMAP_PATH_4 := $(KEYBOARD_PATH_4)/keymaps/$(KEYMAP)
99MAIN_KEYMAP_PATH_5 := $(KEYBOARD_PATH_5)/keymaps/$(KEYMAP) 99MAIN_KEYMAP_PATH_5 := $(KEYBOARD_PATH_5)/keymaps/$(KEYMAP)
100 100
101ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_5)/keymap.c)","") 101# Check for keymap.json first, so we can regenerate keymap.c
102 -include $(MAIN_KEYMAP_PATH_5)/rules.mk 102include build_json.mk
103 KEYMAP_C := $(MAIN_KEYMAP_PATH_5)/keymap.c 103
104 KEYMAP_PATH := $(MAIN_KEYMAP_PATH_5) 104ifeq ("$(wildcard $(KEYMAP_PATH))", "")
105else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_4)/keymap.c)","") 105 # Look through the possible keymap folders until we find a matching keymap.c
106 -include $(MAIN_KEYMAP_PATH_4)/rules.mk 106 ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_5)/keymap.c)","")
107 KEYMAP_C := $(MAIN_KEYMAP_PATH_4)/keymap.c 107 -include $(MAIN_KEYMAP_PATH_5)/rules.mk
108 KEYMAP_PATH := $(MAIN_KEYMAP_PATH_4) 108 KEYMAP_C := $(MAIN_KEYMAP_PATH_5)/keymap.c
109else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_3)/keymap.c)","") 109 KEYMAP_PATH := $(MAIN_KEYMAP_PATH_5)
110 -include $(MAIN_KEYMAP_PATH_3)/rules.mk 110 else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_4)/keymap.c)","")
111 KEYMAP_C := $(MAIN_KEYMAP_PATH_3)/keymap.c 111 -include $(MAIN_KEYMAP_PATH_4)/rules.mk
112 KEYMAP_PATH := $(MAIN_KEYMAP_PATH_3) 112 KEYMAP_C := $(MAIN_KEYMAP_PATH_4)/keymap.c
113else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_2)/keymap.c)","") 113 KEYMAP_PATH := $(MAIN_KEYMAP_PATH_4)
114 -include $(MAIN_KEYMAP_PATH_2)/rules.mk 114 else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_3)/keymap.c)","")
115 KEYMAP_C := $(MAIN_KEYMAP_PATH_2)/keymap.c 115 -include $(MAIN_KEYMAP_PATH_3)/rules.mk
116 KEYMAP_PATH := $(MAIN_KEYMAP_PATH_2) 116 KEYMAP_C := $(MAIN_KEYMAP_PATH_3)/keymap.c
117else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_1)/keymap.c)","") 117 KEYMAP_PATH := $(MAIN_KEYMAP_PATH_3)
118 -include $(MAIN_KEYMAP_PATH_1)/rules.mk 118 else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_2)/keymap.c)","")
119 KEYMAP_C := $(MAIN_KEYMAP_PATH_1)/keymap.c 119 -include $(MAIN_KEYMAP_PATH_2)/rules.mk
120 KEYMAP_PATH := $(MAIN_KEYMAP_PATH_1) 120 KEYMAP_C := $(MAIN_KEYMAP_PATH_2)/keymap.c
121else ifneq ($(LAYOUTS),) 121 KEYMAP_PATH := $(MAIN_KEYMAP_PATH_2)
122 include build_layout.mk 122 else ifneq ("$(wildcard $(MAIN_KEYMAP_PATH_1)/keymap.c)","")
123else 123 -include $(MAIN_KEYMAP_PATH_1)/rules.mk
124 $(error Could not find keymap) 124 KEYMAP_C := $(MAIN_KEYMAP_PATH_1)/keymap.c
125 # this state should never be reached 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
126endif 133endif
127 134
128ifeq ($(strip $(CTPC)), yes) 135ifeq ($(strip $(CTPC)), yes)
@@ -313,7 +320,6 @@ ifneq ("$(wildcard $(USER_PATH)/config.h)","")
313 CONFIG_H += $(USER_PATH)/config.h 320 CONFIG_H += $(USER_PATH)/config.h
314endif 321endif
315 322
316
317# Object files directory 323# Object files directory
318# To put object files in current directory, use a dot (.), do NOT make 324# To put object files in current directory, use a dot (.), do NOT make
319# this an empty or blank macro! 325# this an empty or blank macro!
@@ -323,7 +329,7 @@ ifneq ("$(wildcard $(KEYMAP_PATH)/config.h)","")
323 CONFIG_H += $(KEYMAP_PATH)/config.h 329 CONFIG_H += $(KEYMAP_PATH)/config.h
324endif 330endif
325 331
326# # project specific files 332# project specific files
327SRC += $(KEYBOARD_SRC) \ 333SRC += $(KEYBOARD_SRC) \
328 $(KEYMAP_C) \ 334 $(KEYMAP_C) \
329 $(QUANTUM_SRC) 335 $(QUANTUM_SRC)
diff --git a/docs/_summary.md b/docs/_summary.md
index 8a40ccd7f..611c283ac 100644
--- a/docs/_summary.md
+++ b/docs/_summary.md
@@ -8,6 +8,7 @@
8 8
9* [QMK Basics](README.md) 9* [QMK Basics](README.md)
10 * [QMK Introduction](getting_started_introduction.md) 10 * [QMK Introduction](getting_started_introduction.md)
11 * [QMK CLI](cli.md)
11 * [Contributing to QMK](contributing.md) 12 * [Contributing to QMK](contributing.md)
12 * [How to Use Github](getting_started_github.md) 13 * [How to Use Github](getting_started_github.md)
13 * [Getting Help](getting_started_getting_help.md) 14 * [Getting Help](getting_started_getting_help.md)
@@ -34,6 +35,8 @@
34 * [Keyboard Guidelines](hardware_keyboard_guidelines.md) 35 * [Keyboard Guidelines](hardware_keyboard_guidelines.md)
35 * [Config Options](config_options.md) 36 * [Config Options](config_options.md)
36 * [Keycodes](keycodes.md) 37 * [Keycodes](keycodes.md)
38 * [Coding Conventions - C](coding_conventions_c.md)
39 * [Coding Conventions - Python](coding_conventions_python.md)
37 * [Documentation Best Practices](documentation_best_practices.md) 40 * [Documentation Best Practices](documentation_best_practices.md)
38 * [Documentation Templates](documentation_templates.md) 41 * [Documentation Templates](documentation_templates.md)
39 * [Glossary](reference_glossary.md) 42 * [Glossary](reference_glossary.md)
@@ -41,6 +44,7 @@
41 * [Useful Functions](ref_functions.md) 44 * [Useful Functions](ref_functions.md)
42 * [Configurator Support](reference_configurator_support.md) 45 * [Configurator Support](reference_configurator_support.md)
43 * [info.json Format](reference_info_json.md) 46 * [info.json Format](reference_info_json.md)
47 * [Python Development](python_development.md)
44 48
45* [Features](features.md) 49* [Features](features.md)
46 * [Basic Keycodes](keycodes_basic.md) 50 * [Basic Keycodes](keycodes_basic.md)
diff --git a/docs/cli.md b/docs/cli.md
new file mode 100644
index 000000000..0365f2c9c
--- /dev/null
+++ b/docs/cli.md
@@ -0,0 +1,31 @@
1# QMK CLI
2
3This page describes how to setup and use the QMK CLI.
4
5# Overview
6
7The 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
13Simply add the `qmk_firmware/bin` directory to your `PATH`. You can run the `qmk` commands from any directory.
14
15```
16export PATH=$PATH:$HOME/qmk_firmware/bin
17```
18
19You 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
25This command allows you to compile JSON files you have downloaded from <https://config.qmk.fm>.
26
27**Usage**:
28
29```
30qmk compile-json mine.json
31```
diff --git a/docs/coding_conventions_c.md b/docs/coding_conventions_c.md
new file mode 100644
index 000000000..cbddedf8b
--- /dev/null
+++ b/docs/coding_conventions_c.md
@@ -0,0 +1,58 @@
1# Coding Conventions (C)
2
3Most 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
29Here is an example for easy reference:
30
31```c
32/* Enums for foo */
33enum foo_state {
34 FOO_BAR,
35 FOO_BAZ,
36};
37
38/* Returns a value */
39int 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
52Use the [full LLVM installer](http://llvm.org/builds/) to get clang-format on Windows, or use `sudo apt install clang-format` on Ubuntu.
53
54If 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
56If 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
58Some 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`.
diff --git a/docs/coding_conventions_python.md b/docs/coding_conventions_python.md
new file mode 100644
index 000000000..c7743050e
--- /dev/null
+++ b/docs/coding_conventions_python.md
@@ -0,0 +1,314 @@
1# Coding Conventions (Python)
2
3Most of our style follows PEP8 with some local modifications to make things less nit-picky.
4
5* We target Python 3.5 for compatability with all supported platforms.
6* We indent using four (4) spaces (soft tabs)
7* We encourage liberal use of comments
8 * Think of them as a story describing the feature
9 * Use them liberally to explain why particular decisions were made.
10 * Do not write obvious comments
11 * If you not sure if a comment is obvious, go ahead and include it.
12* We require useful docstrings for all functions.
13* 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.
14* Some of our practices conflict with the wider python community to make our codebase more approachable to non-pythonistas.
15
16# YAPF
17
18You can use [yapf](https://github.com/google/yapf) to style your code. We provide a config in [setup.cfg](setup.cfg).
19
20# Imports
21
22We don't have a hard and fast rule for when to use `import ...` vs `from ... import ...`. Understandability and maintainability is our ultimate goal.
23
24Generally we prefer to import specific function and class names from a module to keep code shorter and easier to understand. Sometimes this results in a name that is ambiguous, and in such cases we prefer to import the module instead. You should avoid using the "as" keyword when importing, unless you are importing a compatability module.
25
26Imports should be one line per module. We group import statements together using the standard python rules- system, 3rd party, local.
27
28Do not use `from foo import *`. Supply a list of objects you want to import instead, or import the whole module.
29
30## Import Examples
31
32Good:
33
34```
35from qmk import effects
36
37effects.echo()
38```
39
40Bad:
41
42```
43from qmk.effects import echo
44
45echo() # It's unclear where echo comes from
46```
47
48Good:
49
50```
51from qmk.keymap import compile_firmware
52
53compile_firmware()
54```
55
56OK, but the above is better:
57
58```
59import qmk.keymap
60
61qmk.keymap.compile_firmware()
62```
63
64# Statements
65
66One statement per line.
67
68Even when allowed (EG `if foo: bar`) we do not combine 2 statements onto a single line.
69
70# Naming
71
72`module_name`, `package_name`, `ClassName`, `method_name`, `ExceptionName`, `function_name`, `GLOBAL_CONSTANT_NAME`, `global_var_name`, `instance_var_name`, `function_parameter_name`, `local_var_name`.
73
74Function names, variable names, and filenames should be descriptive; eschew abbreviation. In particular, do not use abbreviations that are ambiguous or unfamiliar to readers outside your project, and do not abbreviate by deleting letters within a word.
75
76Always use a .py filename extension. Never use dashes.
77
78## Names to Avoid
79
80* single character names except for counters or iterators. You may use "e" as an exception identifier in try/except statements.
81* dashes (-) in any package/module name
82* __double_leading_and_trailing_underscore__ names (reserved by Python)
83
84# Docstrings
85
86To maintain consistency with our docstrings we've set out the following guidelines.
87
88* Use markdown formatting
89* Always use triple-dquote docstrings with at least one linebreak: `"""\n"""`
90* First line is a short (< 70 char) description of what the function does
91* If you need more in your docstring leave a blank line between the description and the rest.
92* Start indented lines at the same indent level as the opening triple-dquote
93* Document all function arguments using the format described below
94* If present, Args:, Returns:, and Raises: should be the last three things in the docstring, separated by a blank line each.
95
96## Simple docstring example
97
98```
99def my_awesome_function():
100 """Return the number of seconds since 1970 Jan 1 00:00 UTC.
101 """
102 return int(time.time())
103```
104
105## Complex docstring example
106
107```
108def my_awesome_function():
109 """Return the number of seconds since 1970 Jan 1 00:00 UTC.
110
111 This function always returns an integer number of seconds.
112 """
113 return int(time.time())
114```
115
116## Function arguments docstring example
117
118```
119def my_awesome_function(start=None, offset=0):
120 """Return the number of seconds since 1970 Jan 1 00:00 UTC.
121
122 This function always returns an integer number of seconds.
123
124
125 Args:
126 start
127 The time to start at instead of 1970 Jan 1 00:00 UTC
128
129 offset
130 Return an answer that has this number of seconds subtracted first
131
132 Returns:
133 An integer describing a number of seconds.
134
135 Raises:
136 ValueError
137 When `start` or `offset` are not positive numbers
138 """
139 if start < 0 or offset < 0:
140 raise ValueError('start and offset must be positive numbers.')
141
142 if not start:
143 start = time.time()
144
145 return int(start - offset)
146```
147
148# Exceptions
149
150Exceptions are used to handle exceptional situations. They should not be used for flow control. This is a break from the python norm of "ask for forgiveness." If you are catching an exception it should be to handle a situation that is unusual.
151
152If you use a catch-all exception for any reason you must log the exception and stacktrace using cli.log.
153
154Make your try/except blocks as short as possible. If you need a lot of try statements you may need to restructure your code.
155
156# Tuples
157
158When defining one-item tuples always include a trailing comma so that it is obvious you are using a tuple. Do not rely on implicit one-item tuple unpacking. Better still use a list which is unambiguous.
159
160This is particularly important when using the printf-style format strings that are commonly used.
161
162# Lists and Dictionaries
163
164We have configured YAPF to differentiate between sequence styles with a trailing comma. When a trailing comma is omitted YAPF will format the sequence as a single line. When a trailing comma is included YAPF will format the sequence with one item per line.
165
166You should generally prefer to keep short definition on a single line. Break out to multiple lines sooner rather than later to aid readability and maintainability.
167
168# Parentheses
169
170Avoid excessive parentheses, but do use parentheses to make code easier to understand. Do not use them in return statements unless you are explicitly returning a tuple, or it is part of a math expression.
171
172# Format Strings
173
174We generally prefer printf-style format strings. Example:
175
176```
177name = 'World'
178print('Hello, %s!' % (name,))
179```
180
181This style is used by the logging module, which we make use of extensively, and we have adopted it in other places for consistency. It is also more familiar to C programmers, who are a big part of our casual audience.
182
183Our included CLI module has support for using these without using the percent (%) operator. Look at `cli.echo()` and the various `cli.log` functions (EG, `cli.log.info()`) for more details.
184
185# Comprehensions & Generator Expressions
186
187We encourage the liberal use of comprehensions and generators, but do not let them get too complex. If you need complexity fall back to a for loop that is easier to understand.
188
189# Lambdas
190
191OK to use but probably should be avoided. With comprehensions and generators the need for lambdas is not as strong as it once was.
192
193# Conditional Expressions
194
195OK in variable assignment, but otherwise should be avoided.
196
197Conditional expressions are if statements that are in line with code. For example:
198
199```
200x = 1 if cond else 2
201```
202
203It's generally not a good idea to use these as function arguments, sequence items, etc. It's too easy to overlook.
204
205# Default Argument Values
206
207Encouraged, but values must be immutable objects.
208
209When specifying default values in argument lists always be careful to specify objects that can't be modified in place. If you use a mutable object the changes you make will persist between calls, which is usually not what you want. Even if that is what you intend to do it is confusing for others and will hinder understanding.
210
211Bad:
212
213```
214def my_func(foo={}):
215 pass
216```
217
218Good:
219
220```
221def my_func(foo=None):
222 if not foo:
223 foo = {}
224```
225
226# Properties
227
228Always use properties instead of getter and setter functions.
229
230```
231class Foo(object):
232 def __init__(self):
233 self._bar = None
234
235 @property
236 def bar(self):
237 return self._bar
238
239 @bar.setter
240 def bar(self, bar):
241 self._bar = bar
242```
243
244# True/False Evaluations
245
246You should generally prefer the implicit True/False evaluation in if statements, rather than checking equivalency.
247
248Bad:
249
250```
251if foo == True:
252 pass
253
254if bar == False:
255 pass
256```
257
258Good:
259
260```
261if foo:
262 pass
263
264if not bar:
265 pass
266```
267
268# Decorators
269
270Use when appropriate. Try to avoid too much magic unless it helps with understanding.
271
272# Threading and Multiprocessing
273
274Should be avoided. If you need this you will have to make a strong case before we merge your code.
275
276# Power Features
277
278Python is an extremely flexible language and gives you many fancy features such as custom metaclasses, access to bytecode, on-the-fly compilation, dynamic inheritance, object reparenting, import hacks, reflection, modification of system internals, etc.
279
280Don't use these.
281
282Performance is not a critical concern for us, and code understandability is. We want our codebase to be approachable by someone who only has a day or two to play with it. These features generally come with a cost to easy understanding, and we would prefer to have code that can be readily understood over faster or more compact code.
283
284Note that some standard library modules use these techniques and it is ok to make use of those modules. But please keep readability and understandability in mind when using them.
285
286# Type Annotated Code
287
288For now we are not using any type annotation system, and would prefer that code remain unannotated. We may revisit this in the future.
289
290# Function length
291
292Prefer small and focused functions.
293
294We recognize that long functions are sometimes appropriate, so no hard limit is placed on function length. If a function exceeds about 40 lines, think about whether it can be broken up without harming the structure of the program.
295
296Even if your long function works perfectly now, someone modifying it in a few months may add new behavior. This could result in bugs that are hard to find. Keeping your functions short and simple makes it easier for other people to read and modify your code.
297
298You could find long and complicated functions when working with some code. Do not be intimidated by modifying existing code: if working with such a function proves to be difficult, you find that errors are hard to debug, or you want to use a piece of it in several different contexts, consider breaking up the function into smaller and more manageable pieces.
299
300# FIXMEs
301
302It is OK to leave FIXMEs in code. Why? Encouraging people to at least document parts of code that need to be thought out more (or that are confusing) is better than leaving this code undocumented.
303
304All FIXMEs should be formatted like:
305
306```
307FIXME(username): Revisit this code when the frob feature is done.
308```
309
310...where username is your GitHub username.
311
312# Unit Tests
313
314These are good. We should have some one day.
diff --git a/docs/contributing.md b/docs/contributing.md
index 7d1a9691c..761bc9959 100644
--- a/docs/contributing.md
+++ b/docs/contributing.md
@@ -54,62 +54,10 @@ Never made an open source contribution before? Wondering how contributions work
54 54
55# Coding Conventions 55# Coding Conventions
56 56
57Most 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: 57Most of our style is pretty easy to pick up on. If you are familiar with either C or Python you should not have too much trouble with our local styles.
58
59* We indent using four (4) spaces (soft tabs)
60* We use a modified One True Brace Style
61 * Opening Brace: At the end of the same line as the statement that opens the block
62 * Closing Brace: Lined up with the first character of the statement that opens the block
63 * Else If: Place the closing brace at the beginning of the line and the next opening brace at the end of the same line.
64 * Optional Braces: Always include optional braces.
65 * Good: if (condition) { return false; }
66 * Bad: if (condition) return false;
67* We encourage use of C style comments: `/* */`
68 * Think of them as a story describing the feature
69 * Use them liberally to explain why particular decisions were made.
70 * Do not write obvious comments
71 * If you not sure if a comment is obvious, go ahead and include it.
72* 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.
73* 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`)
74* We accept both forms of preprocessor if's: `#ifdef DEFINED` and `#if defined(DEFINED)`
75 * If you are not sure which to prefer use the `#if defined(DEFINED)` form.
76 * Do not change existing code from one style to the other, except when moving to a multiple condition `#if`.
77 * Do not put whitespace between `#` and `if`.
78 * When deciding how (or if) to indent directives keep these points in mind:
79 * Readability is more important than consistency.
80 * Follow the file's existing style. If the file is mixed follow the style that makes sense for the section you are modifying.
81 * 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.
82
83Here is an example for easy reference:
84 58
85```c 59* [Coding Conventions - C](coding_conventions_c.md)
86/* Enums for foo */ 60* [Coding Conventions - Python](coding_conventions_python.md)
87enum foo_state {
88 FOO_BAR,
89 FOO_BAZ,
90};
91
92/* Returns a value */
93int foo(void) {
94 if (some_condition) {
95 return FOO_BAR;
96 } else {
97 return -1;
98 }
99}
100```
101
102# Auto-formatting with clang-format
103
104[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.
105
106Use the [full LLVM installer](http://llvm.org/builds/) to get clang-format on Windows, or use `sudo apt install clang-format` on Ubuntu.
107
108If 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.
109
110If 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.
111
112Some 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`.
113 61
114# General Guidelines 62# General Guidelines
115 63
diff --git a/docs/python_development.md b/docs/python_development.md
new file mode 100644
index 000000000..b976a7c0e
--- /dev/null
+++ b/docs/python_development.md
@@ -0,0 +1,45 @@
1# Python Development in QMK
2
3This document gives an overview of how QMK has structured its python code. You should read this before working on any of the python code.
4
5## Script directories
6
7There are two places scripts live in QMK: `qmk_firmware/bin` and `qmk_firmware/util`. You should use `bin` for any python scripts that utilize the `qmk` wrapper. Scripts that are standalone and not run very often live in `util`.
8
9We discourage putting anything into `bin` that does not utilize the `qmk` wrapper. If you think you have a good reason for doing so please talk to us about your use case.
10
11## Python Modules
12
13Most of the QMK python modules can be found in `qmk_firmware/lib/python`. This is the path that we append to `sys.path`.
14
15We have a module hierarchy under that path:
16
17* `qmk_firmware/lib/python`
18 * `milc.py` - The CLI library we use. Will be pulled out into its own module in the future.
19 * `qmk` - Code associated with QMK
20 * `cli` - Modules that will be imported for CLI commands.
21 * `errors.py` - Errors that can be raised within QMK apps
22 * `keymap.py` - Functions for working with keymaps
23
24## CLI Scripts
25
26We have a CLI wrapper that you should utilize for any user facing scripts. We think it's pretty easy to use and it gives you a lot of nice things for free.
27
28To use the wrapper simply place a module into `qmk_firmware/lib/python/qmk/cli`, and create a symlink to `bin/qmk` named after your module. Dashes in command names will be converted into dots so you can use hierarchy to manage commands.
29
30When `qmk` is run it checks to see how it was invoked. If it was invoked as `qmk` the module name is take from `sys.argv[1]`. If it was invoked as `qmk-<module-name>` then everything after the first dash is taken as the module name. Dashes and underscores are converted to dots, and then `qmk.cli` is prepended before the module is imported.
31
32The module uses `@cli.entrypoint()` and `@cli.argument()` decorators to define an entrypoint, which is where execution starts.
33
34## Example CLI Script
35
36We have provided a QMK Hello World script you can use as an example. To run it simply run `qmk hello` or `qmk-hello`. The source code is listed below.
37
38```
39from milc import cli
40
41@cli.argument('-n', '--name', default='World', help='Name to greet.')
42@cli.entrypoint('QMK Python Hello World.')
43def main(cli):
44 cli.echo('Hello, %s!', cli.config.general.name)
45```
diff --git a/keyboards/clueboard/66_hotswap/keymaps/json/keymap.json b/keyboards/clueboard/66_hotswap/keymaps/json/keymap.json
new file mode 100644
index 000000000..20aa9f0f6
--- /dev/null
+++ b/keyboards/clueboard/66_hotswap/keymaps/json/keymap.json
@@ -0,0 +1 @@
{"keyboard":"clueboard/66_hotswap/gen1","keymap":"default_66","layout":"LAYOUT","layers":[["KC_GESC","KC_1","KC_2","KC_3","KC_4","KC_5","KC_6","KC_7","KC_8","KC_9","KC_0","KC_MINS","KC_EQL","KC_BSPC","KC_PGUP","KC_TAB","KC_Q","KC_W","KC_E","KC_R","KC_T","KC_Y","KC_U","KC_I","KC_O","KC_P","KC_LBRC","KC_RBRC","KC_BSLS","KC_PGDN","KC_CAPS","KC_A","KC_S","KC_D","KC_F","KC_G","KC_H","KC_J","KC_K","KC_L","KC_SCLN","KC_QUOT","KC_ENT","KC_LSFT","KC_Z","KC_X","KC_C","KC_V","KC_B","KC_N","KC_M","KC_COMM","KC_DOT","KC_SLSH","KC_RSFT","KC_UP","KC_LCTL","KC_LGUI","KC_LALT","KC_SPC","KC_SPC","KC_RALT","KC_RGUI","MO(1)","KC_RCTL","KC_LEFT","KC_DOWN","KC_RGHT"],["KC_GRV","KC_F1","KC_F2","KC_F3","KC_F4","KC_F5","KC_F6","KC_F7","KC_F8","KC_F9","KC_F10","KC_F11","KC_F12","KC_DEL","BL_INC","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_MPRV","KC_MPLY","KC_MNXT","KC_NO","KC_MUTE","BL_DEC","KC_NO","KC_NO","MO(2)","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_PGUP","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","MO(1)","KC_NO","KC_HOME","KC_PGDN","KC_END"],["KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","BL_TOGG","BL_INC","KC_NO","KC_NO","KC_NO","KC_NO","RESET","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","BL_DEC","KC_NO","KC_NO","MO(2)","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","KC_NO","BL_STEP","KC_NO","KC_NO","MO(1)","KC_NO","KC_NO","KC_NO","KC_NO"]],"author":"","notes":""} \ No newline at end of file
diff --git a/lib/python/milc.py b/lib/python/milc.py
new file mode 100644
index 000000000..6e82edf8b
--- /dev/null
+++ b/lib/python/milc.py
@@ -0,0 +1,716 @@
1#!/usr/bin/env python3
2# coding=utf-8
3"""MILC - A CLI Framework
4
5PYTHON_ARGCOMPLETE_OK
6
7MILC is an opinionated framework for writing CLI apps. It optimizes for the
8most common unix tool pattern- small tools that are run from the command
9line but generally do not feature any user interaction while they run.
10
11For more details see the MILC documentation:
12
13 <https://github.com/clueboard/milc/tree/master/docs>
14"""
15from __future__ import division, print_function, unicode_literals
16import argparse
17import logging
18import os
19import re
20import sys
21from decimal import Decimal
22from tempfile import NamedTemporaryFile
23from time import sleep
24
25try:
26 from ConfigParser import RawConfigParser
27except ImportError:
28 from configparser import RawConfigParser
29
30try:
31 import thread
32 import threading
33except ImportError:
34 thread = None
35
36import argcomplete
37import colorama
38
39# Log Level Representations
40EMOJI_LOGLEVELS = {
41 'CRITICAL': '{bg_red}{fg_white}¬_¬{style_reset_all}',
42 'ERROR': '{fg_red}☒{style_reset_all}',
43 'WARNING': '{fg_yellow}⚠{style_reset_all}',
44 'INFO': '{fg_blue}ℹ{style_reset_all}',
45 'DEBUG': '{fg_cyan}☐{style_reset_all}',
46 'NOTSET': '{style_reset_all}¯\\_(o_o)_/¯'
47}
48EMOJI_LOGLEVELS['FATAL'] = EMOJI_LOGLEVELS['CRITICAL']
49EMOJI_LOGLEVELS['WARN'] = EMOJI_LOGLEVELS['WARNING']
50
51# ANSI Color setup
52# Regex was gratefully borrowed from kfir on stackoverflow:
53# https://stackoverflow.com/a/45448194
54ansi_regex = r'\x1b(' \
55 r'(\[\??\d+[hl])|' \
56 r'([=<>a-kzNM78])|' \
57 r'([\(\)][a-b0-2])|' \
58 r'(\[\d{0,2}[ma-dgkjqi])|' \
59 r'(\[\d+;\d+[hfy]?)|' \
60 r'(\[;?[hf])|' \
61 r'(#[3-68])|' \
62 r'([01356]n)|' \
63 r'(O[mlnp-z]?)|' \
64 r'(/Z)|' \
65 r'(\d+)|' \
66 r'(\[\?\d;\d0c)|' \
67 r'(\d;\dR))'
68ansi_escape = re.compile(ansi_regex, flags=re.IGNORECASE)
69ansi_styles = (
70 ('fg', colorama.ansi.AnsiFore()),
71 ('bg', colorama.ansi.AnsiBack()),
72 ('style', colorama.ansi.AnsiStyle()),
73)
74ansi_colors = {}
75
76for prefix, obj in ansi_styles:
77 for color in [x for x in obj.__dict__ if not x.startswith('_')]:
78 ansi_colors[prefix + '_' + color.lower()] = getattr(obj, color)
79
80
81def format_ansi(text):
82 """Return a copy of text with certain strings replaced with ansi.
83 """
84 # Avoid .format() so we don't have to worry about the log content
85 for color in ansi_colors:
86 text = text.replace('{%s}' % color, ansi_colors[color])
87 return text + ansi_colors['style_reset_all']
88
89
90class ANSIFormatter(logging.Formatter):
91 """A log formatter that inserts ANSI color.
92 """
93
94 def format(self, record):
95 msg = super(ANSIFormatter, self).format(record)
96 return format_ansi(msg)
97
98
99class ANSIEmojiLoglevelFormatter(ANSIFormatter):
100 """A log formatter that makes the loglevel an emoji.
101 """
102
103 def format(self, record):
104 record.levelname = EMOJI_LOGLEVELS[record.levelname].format(**ansi_colors)
105 return super(ANSIEmojiLoglevelFormatter, self).format(record)
106
107
108class ANSIStrippingFormatter(ANSIFormatter):
109 """A log formatter that strips ANSI.
110 """
111
112 def format(self, record):
113 msg = super(ANSIStrippingFormatter, self).format(record)
114 return ansi_escape.sub('', msg)
115
116
117class Configuration(object):
118 """Represents the running configuration.
119
120 This class never raises IndexError, instead it will return None if a
121 section or option does not yet exist.
122 """
123
124 def __contains__(self, key):
125 return self._config.__contains__(key)
126
127 def __iter__(self):
128 return self._config.__iter__()
129
130 def __len__(self):
131 return self._config.__len__()
132
133 def __repr__(self):
134 return self._config.__repr__()
135
136 def keys(self):
137 return self._config.keys()
138
139 def items(self):
140 return self._config.items()
141
142 def values(self):
143 return self._config.values()
144
145 def __init__(self, *args, **kwargs):
146 self._config = {}
147 self.default_container = ConfigurationOption
148
149 def __getitem__(self, key):
150 """Returns a config section, creating it if it doesn't exist yet.
151 """
152 if key not in self._config:
153 self.__dict__[key] = self._config[key] = ConfigurationOption()
154
155 return self._config[key]
156
157 def __setitem__(self, key, value):
158 self.__dict__[key] = value
159 self._config[key] = value
160
161 def __delitem__(self, key):
162 if key in self.__dict__ and key[0] != '_':
163 del self.__dict__[key]
164 del self._config[key]
165
166
167class ConfigurationOption(Configuration):
168 def __init__(self, *args, **kwargs):
169 super(ConfigurationOption, self).__init__(*args, **kwargs)
170 self.default_container = dict
171
172 def __getitem__(self, key):
173 """Returns a config section, creating it if it doesn't exist yet.
174 """
175 if key not in self._config:
176 self.__dict__[key] = self._config[key] = None
177
178 return self._config[key]
179
180
181def handle_store_boolean(self, *args, **kwargs):
182 """Does the add_argument for action='store_boolean'.
183 """
184 kwargs['add_dest'] = False
185 disabled_args = None
186 disabled_kwargs = kwargs.copy()
187 disabled_kwargs['action'] = 'store_false'
188 disabled_kwargs['help'] = 'Disable ' + kwargs['help']
189 kwargs['action'] = 'store_true'
190 kwargs['help'] = 'Enable ' + kwargs['help']
191
192 for flag in args:
193 if flag[:2] == '--':
194 disabled_args = ('--no-' + flag[2:],)
195 break
196
197 self.add_argument(*args, **kwargs)
198 self.add_argument(*disabled_args, **disabled_kwargs)
199
200 return (args, kwargs, disabled_args, disabled_kwargs)
201
202
203class SubparserWrapper(object):
204 """Wrap subparsers so we can populate the normal and the shadow parser.
205 """
206
207 def __init__(self, cli, submodule, subparser):
208 self.cli = cli
209 self.submodule = submodule
210 self.subparser = subparser
211
212 for attr in dir(subparser):
213 if not hasattr(self, attr):
214 setattr(self, attr, getattr(subparser, attr))
215
216 def completer(self, completer):
217 """Add an arpcomplete completer to this subcommand.
218 """
219 self.subparser.completer = completer
220
221 def add_argument(self, *args, **kwargs):
222 if kwargs.get('add_dest', True):
223 kwargs['dest'] = self.submodule + '_' + self.cli.get_argument_name(*args, **kwargs)
224 if 'add_dest' in kwargs:
225 del kwargs['add_dest']
226
227 if 'action' in kwargs and kwargs['action'] == 'store_boolean':
228 return handle_store_boolean(self, *args, **kwargs)
229
230 self.cli.acquire_lock()
231 self.subparser.add_argument(*args, **kwargs)
232
233 if 'default' in kwargs:
234 del kwargs['default']
235 if 'action' in kwargs and kwargs['action'] == 'store_false':
236 kwargs['action'] == 'store_true'
237 self.cli.subcommands_default[self.submodule].add_argument(*args, **kwargs)
238 self.cli.release_lock()
239
240
241class MILC(object):
242 """MILC - An Opinionated Batteries Included Framework
243 """
244
245 def __init__(self):
246 """Initialize the MILC object.
247 """
248 # Setup a lock for thread safety
249 self._lock = threading.RLock() if thread else None
250
251 # Define some basic info
252 self.acquire_lock()
253 self._description = None
254 self._entrypoint = None
255 self._inside_context_manager = False
256 self.ansi = ansi_colors
257 self.config = Configuration()
258 self.config_file = None
259 self.prog_name = sys.argv[0][:-3] if sys.argv[0].endswith('.py') else sys.argv[0]
260 self.version = os.environ.get('QMK_VERSION', 'unknown')
261 self.release_lock()
262
263 # Initialize all the things
264 self.initialize_argparse()
265 self.initialize_logging()
266
267 @property
268 def description(self):
269 return self._description
270
271 @description.setter
272 def description(self, value):
273 self._description = self._arg_parser.description = self._arg_defaults.description = value
274
275 def echo(self, text, *args, **kwargs):
276 """Print colorized text to stdout, as long as stdout is a tty.
277
278 ANSI color strings (such as {fg-blue}) will be converted into ANSI
279 escape sequences, and the ANSI reset sequence will be added to all
280 strings.
281
282 If *args or **kwargs are passed they will be used to %-format the strings.
283 """
284 if args and kwargs:
285 raise RuntimeError('You can only specify *args or **kwargs, not both!')
286
287 if sys.stdout.isatty():
288 args = args or kwargs
289 text = format_ansi(text)
290
291 print(text % args)
292
293 def initialize_argparse(self):
294 """Prepare to process arguments from sys.argv.
295 """
296 kwargs = {
297 'fromfile_prefix_chars': '@',
298 'conflict_handler': 'resolve',
299 }
300
301 self.acquire_lock()
302 self.subcommands = {}
303 self.subcommands_default = {}
304 self._subparsers = None
305 self._subparsers_default = None
306 self.argwarn = argcomplete.warn
307 self.args = None
308 self._arg_defaults = argparse.ArgumentParser(**kwargs)
309 self._arg_parser = argparse.ArgumentParser(**kwargs)
310 self.set_defaults = self._arg_parser.set_defaults
311 self.print_usage = self._arg_parser.print_usage
312 self.print_help = self._arg_parser.print_help
313 self.release_lock()
314
315 def completer(self, completer):
316 """Add an arpcomplete completer to this subcommand.
317 """
318 self._arg_parser.completer = completer
319
320 def add_argument(self, *args, **kwargs):
321 """Wrapper to add arguments to both the main and the shadow argparser.
322 """
323 if kwargs.get('add_dest', True) and args[0][0] == '-':
324 kwargs['dest'] = 'general_' + self.get_argument_name(*args, **kwargs)
325 if 'add_dest' in kwargs:
326 del kwargs['add_dest']
327
328 if 'action' in kwargs and kwargs['action'] == 'store_boolean':
329 return handle_store_boolean(self, *args, **kwargs)
330
331 self.acquire_lock()
332 self._arg_parser.add_argument(*args, **kwargs)
333
334 # Populate the shadow parser
335 if 'default' in kwargs:
336 del kwargs['default']
337 if 'action' in kwargs and kwargs['action'] == 'store_false':
338 kwargs['action'] == 'store_true'
339 self._arg_defaults.add_argument(*args, **kwargs)
340 self.release_lock()
341
342 def initialize_logging(self):
343 """Prepare the defaults for the logging infrastructure.
344 """
345 self.acquire_lock()
346 self.log_file = None
347 self.log_file_mode = 'a'
348 self.log_file_handler = None
349 self.log_print = True
350 self.log_print_to = sys.stderr
351 self.log_print_level = logging.INFO
352 self.log_file_level = logging.DEBUG
353 self.log_level = logging.INFO
354 self.log = logging.getLogger(self.__class__.__name__)
355 self.log.setLevel(logging.DEBUG)
356 logging.root.setLevel(logging.DEBUG)
357 self.release_lock()
358
359 self.add_argument('-V', '--version', version=self.version, action='version', help='Display the version and exit')
360 self.add_argument('-v', '--verbose', action='store_true', help='Make the logging more verbose')
361 self.add_argument('--datetime-fmt', default='%Y-%m-%d %H:%M:%S', help='Format string for datetimes')
362 self.add_argument('--log-fmt', default='%(levelname)s %(message)s', help='Format string for printed log output')
363 self.add_argument('--log-file-fmt', default='[%(levelname)s] [%(asctime)s] [file:%(pathname)s] [line:%(lineno)d] %(message)s', help='Format string for log file.')
364 self.add_argument('--log-file', help='File to write log messages to')
365 self.add_argument('--color', action='store_boolean', default=True, help='color in output')
366 self.add_argument('-c', '--config-file', help='The config file to read and/or write')
367 self.add_argument('--save-config', action='store_true', help='Save the running configuration to the config file')
368
369 def add_subparsers(self, title='Sub-commands', **kwargs):
370 if self._inside_context_manager:
371 raise RuntimeError('You must run this before the with statement!')
372
373 self.acquire_lock()
374 self._subparsers_default = self._arg_defaults.add_subparsers(title=title, dest='subparsers', **kwargs)
375 self._subparsers = self._arg_parser.add_subparsers(title=title, dest='subparsers', **kwargs)
376 self.release_lock()
377
378 def acquire_lock(self):
379 """Acquire the MILC lock for exclusive access to properties.
380 """
381 if self._lock:
382 self._lock.acquire()
383
384 def release_lock(self):
385 """Release the MILC lock.
386 """
387 if self._lock:
388 self._lock.release()
389
390 def find_config_file(self):
391 """Locate the config file.
392 """
393 if self.config_file:
394 return self.config_file
395
396 if self.args and self.args.general_config_file:
397 return self.args.general_config_file
398
399 return os.path.abspath(os.path.expanduser('~/.%s.ini' % self.prog_name))
400
401 def get_argument_name(self, *args, **kwargs):
402 """Takes argparse arguments and returns the dest name.
403 """
404 try:
405 return self._arg_parser._get_optional_kwargs(*args, **kwargs)['dest']
406 except ValueError:
407 return self._arg_parser._get_positional_kwargs(*args, **kwargs)['dest']
408
409 def argument(self, *args, **kwargs):
410 """Decorator to call self.add_argument or self.<subcommand>.add_argument.
411 """
412 if self._inside_context_manager:
413 raise RuntimeError('You must run this before the with statement!')
414
415 def argument_function(handler):
416 if handler is self._entrypoint:
417 self.add_argument(*args, **kwargs)
418
419 elif handler.__name__ in self.subcommands:
420 self.subcommands[handler.__name__].add_argument(*args, **kwargs)
421
422 else:
423 raise RuntimeError('Decorated function is not entrypoint or subcommand!')
424
425 return handler
426
427 return argument_function
428
429 def arg_passed(self, arg):
430 """Returns True if arg was passed on the command line.
431 """
432 return self.args_passed[arg] in (None, False)
433
434 def parse_args(self):
435 """Parse the CLI args.
436 """
437 if self.args:
438 self.log.debug('Warning: Arguments have already been parsed, ignoring duplicate attempt!')
439 return
440
441 argcomplete.autocomplete(self._arg_parser)
442
443 self.acquire_lock()
444 self.args = self._arg_parser.parse_args()
445 self.args_passed = self._arg_defaults.parse_args()
446
447 if 'entrypoint' in self.args:
448 self._entrypoint = self.args.entrypoint
449
450 if self.args.general_config_file:
451 self.config_file = self.args.general_config_file
452
453 self.release_lock()
454
455 def read_config(self):
456 """Parse the configuration file and determine the runtime configuration.
457 """
458 self.acquire_lock()
459 self.config_file = self.find_config_file()
460
461 if self.config_file and os.path.exists(self.config_file):
462 config = RawConfigParser(self.config)
463 config.read(self.config_file)
464
465 # Iterate over the config file options and write them into self.config
466 for section in config.sections():
467 for option in config.options(section):
468 value = config.get(section, option)
469
470 # Coerce values into useful datatypes
471 if value.lower() in ['1', 'yes', 'true', 'on']:
472 value = True
473 elif value.lower() in ['0', 'no', 'false', 'none', 'off']:
474 value = False
475 elif value.replace('.', '').isdigit():
476 if '.' in value:
477 value = Decimal(value)
478 else:
479 value = int(value)
480
481 self.config[section][option] = value
482
483 # Fold the CLI args into self.config
484 for argument in vars(self.args):
485 if argument in ('subparsers', 'entrypoint'):
486 continue
487
488 if '_' not in argument:
489 continue
490
491 section, option = argument.split('_', 1)
492 if hasattr(self.args_passed, argument):
493 self.config[section][option] = getattr(self.args, argument)
494 else:
495 if option not in self.config[section]:
496 self.config[section][option] = getattr(self.args, argument)
497
498 self.release_lock()
499
500 def save_config(self):
501 """Save the current configuration to the config file.
502 """
503 self.log.debug("Saving config file to '%s'", self.config_file)
504
505 if not self.config_file:
506 self.log.warning('%s.config_file file not set, not saving config!', self.__class__.__name__)
507 return
508
509 self.acquire_lock()
510
511 config = RawConfigParser()
512 for section_name, section in self.config._config.items():
513 config.add_section(section_name)
514 for option_name, value in section.items():
515 if section_name == 'general':
516 if option_name in ['save_config']:
517 continue
518 config.set(section_name, option_name, str(value))
519
520 with NamedTemporaryFile(mode='w', dir=os.path.dirname(self.config_file), delete=False) as tmpfile:
521 config.write(tmpfile)
522
523 # Move the new config file into place atomically
524 if os.path.getsize(tmpfile.name) > 0:
525 os.rename(tmpfile.name, self.config_file)
526 else:
527 self.log.warning('Config file saving failed, not replacing %s with %s.', self.config_file, tmpfile.name)
528
529 self.release_lock()
530
531 def __call__(self):
532 """Execute the entrypoint function.
533 """
534 if not self._inside_context_manager:
535 # If they didn't use the context manager use it ourselves
536 with self:
537 self.__call__()
538 return
539
540 if not self._entrypoint:
541 raise RuntimeError('No entrypoint provided!')
542
543 return self._entrypoint(self)
544
545 def entrypoint(self, description):
546 """Set the entrypoint for when no subcommand is provided.
547 """
548 if self._inside_context_manager:
549 raise RuntimeError('You must run this before cli()!')
550
551 self.acquire_lock()
552 self.description = description
553 self.release_lock()
554
555 def entrypoint_func(handler):
556 self.acquire_lock()
557 self._entrypoint = handler
558 self.release_lock()
559
560 return handler
561
562 return entrypoint_func
563
564 def add_subcommand(self, handler, description, name=None, **kwargs):
565 """Register a subcommand.
566
567 If name is not provided we use `handler.__name__`.
568 """
569 if self._inside_context_manager:
570 raise RuntimeError('You must run this before the with statement!')
571
572 if self._subparsers is None:
573 self.add_subparsers()
574
575 if not name:
576 name = handler.__name__
577
578 self.acquire_lock()
579 kwargs['help'] = description
580 self.subcommands_default[name] = self._subparsers_default.add_parser(name, **kwargs)
581 self.subcommands[name] = SubparserWrapper(self, name, self._subparsers.add_parser(name, **kwargs))
582 self.subcommands[name].set_defaults(entrypoint=handler)
583
584 if name not in self.__dict__:
585 self.__dict__[name] = self.subcommands[name]
586 else:
587 self.log.debug("Could not add subcommand '%s' to attributes, key already exists!", name)
588
589 self.release_lock()
590
591 return handler
592
593 def subcommand(self, description, **kwargs):
594 """Decorator to register a subcommand.
595 """
596
597 def subcommand_function(handler):
598 return self.add_subcommand(handler, description, **kwargs)
599
600 return subcommand_function
601
602 def setup_logging(self):
603 """Called by __enter__() to setup the logging configuration.
604 """
605 if len(logging.root.handlers) != 0:
606 # This is not a design decision. This is what I'm doing for now until I can examine and think about this situation in more detail.
607 raise RuntimeError('MILC should be the only system installing root log handlers!')
608
609 self.acquire_lock()
610
611 if self.config['general']['verbose']:
612 self.log_print_level = logging.DEBUG
613
614 self.log_file = self.config['general']['log_file'] or self.log_file
615 self.log_file_format = self.config['general']['log_file_fmt']
616 self.log_file_format = ANSIStrippingFormatter(self.config['general']['log_file_fmt'], self.config['general']['datetime_fmt'])
617 self.log_format = self.config['general']['log_fmt']
618
619 if self.config.general.color:
620 self.log_format = ANSIEmojiLoglevelFormatter(self.args.general_log_fmt, self.config.general.datetime_fmt)
621 else:
622 self.log_format = ANSIStrippingFormatter(self.args.general_log_fmt, self.config.general.datetime_fmt)
623
624 if self.log_file:
625 self.log_file_handler = logging.FileHandler(self.log_file, self.log_file_mode)
626 self.log_file_handler.setLevel(self.log_file_level)
627 self.log_file_handler.setFormatter(self.log_file_format)
628 logging.root.addHandler(self.log_file_handler)
629
630 if self.log_print:
631 self.log_print_handler = logging.StreamHandler(self.log_print_to)
632 self.log_print_handler.setLevel(self.log_print_level)
633 self.log_print_handler.setFormatter(self.log_format)
634 logging.root.addHandler(self.log_print_handler)
635
636 self.release_lock()
637
638 def __enter__(self):
639 if self._inside_context_manager:
640 self.log.debug('Warning: context manager was entered again. This usually means that self.__call__() was called before the with statement. You probably do not want to do that.')
641 return
642
643 self.acquire_lock()
644 self._inside_context_manager = True
645 self.release_lock()
646
647 colorama.init()
648 self.parse_args()
649 self.read_config()
650 self.setup_logging()
651
652 if self.config.general.save_config:
653 self.save_config()
654
655 return self
656
657 def __exit__(self, exc_type, exc_val, exc_tb):
658 self.acquire_lock()
659 self._inside_context_manager = False
660 self.release_lock()
661
662 if exc_type is not None and not isinstance(SystemExit(), exc_type):
663 print(exc_type)
664 logging.exception(exc_val)
665 exit(255)
666
667
668cli = MILC()
669
670if __name__ == '__main__':
671
672 @cli.argument('-c', '--comma', help='comma in output', default=True, action='store_boolean')
673 @cli.entrypoint('My useful CLI tool with subcommands.')
674 def main(cli):
675 comma = ',' if cli.config.general.comma else ''
676 cli.log.info('{bg_green}{fg_red}Hello%s World!', comma)
677
678 @cli.argument('-n', '--name', help='Name to greet', default='World')
679 @cli.subcommand('Description of hello subcommand here.')
680 def hello(cli):
681 comma = ',' if cli.config.general.comma else ''
682 cli.log.info('{fg_blue}Hello%s %s!', comma, cli.config.hello.name)
683
684 def goodbye(cli):
685 comma = ',' if cli.config.general.comma else ''
686 cli.log.info('{bg_red}Goodbye%s %s!', comma, cli.config.goodbye.name)
687
688 @cli.argument('-n', '--name', help='Name to greet', default='World')
689 @cli.subcommand('Think a bit before greeting the user.')
690 def thinking(cli):
691 comma = ',' if cli.config.general.comma else ''
692 spinner = cli.spinner(text='Just a moment...', spinner='earth')
693 spinner.start()
694 sleep(2)
695 spinner.stop()
696
697 with cli.spinner(text='Almost there!', spinner='moon'):
698 sleep(2)
699
700 cli.log.info('{fg_cyan}Hello%s %s!', comma, cli.config.thinking.name)
701
702 @cli.subcommand('Show off our ANSI colors.')
703 def pride(cli):
704 cli.echo('{bg_red} ')
705 cli.echo('{bg_lightred_ex} ')
706 cli.echo('{bg_lightyellow_ex} ')
707 cli.echo('{bg_green} ')
708 cli.echo('{bg_blue} ')
709 cli.echo('{bg_magenta} ')
710
711 # You can register subcommands using decorators as seen above, or using functions like like this:
712 cli.add_subcommand(goodbye, 'This will show up in --help output.')
713 cli.goodbye.add_argument('-n', '--name', help='Name to bid farewell to', default='World')
714
715 cli() # Automatically picks between main(), hello() and goodbye()
716 print(sorted(ansi_colors.keys()))
diff --git a/lib/python/qmk/__init__.py b/lib/python/qmk/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/lib/python/qmk/__init__.py
diff --git a/lib/python/qmk/cli/compile/__init__.py b/lib/python/qmk/cli/compile/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/lib/python/qmk/cli/compile/__init__.py
diff --git a/lib/python/qmk/cli/compile/json.py b/lib/python/qmk/cli/compile/json.py
new file mode 100755
index 000000000..89c16b206
--- /dev/null
+++ b/lib/python/qmk/cli/compile/json.py
@@ -0,0 +1,44 @@
1"""Create a keymap directory from a configurator export.
2"""
3import json
4import os
5import sys
6import subprocess
7
8from milc import cli
9
10import qmk.keymap
11import qmk.path
12
13
14@cli.argument('filename', help='Configurator JSON export')
15@cli.entrypoint('Compile a QMK Configurator export.')
16def main(cli):
17 """Compile a QMK Configurator export.
18
19 This command creates a new keymap from a configurator export, overwriting an existing keymap if one exists.
20
21 FIXME(skullydazed): add code to check and warn if the keymap already exists
22 """
23 # Error checking
24 if cli.args.filename == ('-'):
25 cli.log.error('Reading from STDIN is not (yet) supported.')
26 exit(1)
27 if not os.path.exists(qmk.path.normpath(cli.args.filename)):
28 cli.log.error('JSON file does not exist!')
29 exit(1)
30
31 # Parse the configurator json
32 with open(qmk.path.normpath(cli.args.filename), 'r') as fd:
33 user_keymap = json.load(fd)
34
35 # Generate the keymap
36 keymap_path = qmk.path.keymap(user_keymap['keyboard'])
37 cli.log.info('Creating {fg_cyan}%s{style_reset_all} keymap in {fg_cyan}%s', user_keymap['keymap'], keymap_path)
38 qmk.keymap.write(user_keymap['keyboard'], user_keymap['keymap'], user_keymap['layout'], user_keymap['layers'])
39 cli.log.info('Wrote keymap to {fg_cyan}%s/%s/keymap.c', keymap_path, user_keymap['keymap'])
40
41 # Compile the keymap
42 command = ['make', ':'.join((user_keymap['keyboard'], user_keymap['keymap']))]
43 cli.log.info('Compiling keymap with {fg_cyan}%s\n\n', ' '.join(command))
44 subprocess.run(command)
diff --git a/lib/python/qmk/cli/doctor.py b/lib/python/qmk/cli/doctor.py
new file mode 100755
index 000000000..9ce765a4b
--- /dev/null
+++ b/lib/python/qmk/cli/doctor.py
@@ -0,0 +1,47 @@
1"""QMK Python Doctor
2
3Check up for QMK environment.
4"""
5import shutil
6import platform
7import os
8
9from milc import cli
10
11
12@cli.entrypoint('Basic QMK environment checks')
13def main(cli):
14 """Basic QMK environment checks.
15
16 This is currently very simple, it just checks that all the expected binaries are on your system.
17
18 TODO(unclaimed):
19 * [ ] Run the binaries to make sure they work
20 * [ ] Compile a trivial program with each compiler
21 * [ ] Check for udev entries on linux
22 """
23
24 binaries = ['dfu-programmer', 'avrdude', 'dfu-util', 'avr-gcc', 'arm-none-eabi-gcc']
25
26 cli.log.info('QMK Doctor is Checking your environment')
27
28 ok = True
29 for binary in binaries:
30 res = shutil.which(binary)
31 if res is None:
32 cli.log.error('{fg_red}QMK can\'t find ' + binary + ' in your path')
33 ok = False
34
35 OS = platform.system()
36 if OS == "Darwin":
37 cli.log.info("Detected {fg_cyan}macOS")
38 elif OS == "Linux":
39 cli.log.info("Detected {fg_cyan}linux")
40 test = 'systemctl list-unit-files | grep enabled | grep -i ModemManager'
41 if os.system(test) == 0:
42 cli.log.warn("{bg_yellow}Detected modem manager. Please disable it if you are using Pro Micros")
43 else:
44 cli.log.info("Assuming {fg_cyan}Windows")
45
46 if ok:
47 cli.log.info('{fg_green}QMK is ready to go')
diff --git a/lib/python/qmk/cli/hello.py b/lib/python/qmk/cli/hello.py
new file mode 100755
index 000000000..bc0cb6de1
--- /dev/null
+++ b/lib/python/qmk/cli/hello.py
@@ -0,0 +1,13 @@
1"""QMK Python Hello World
2
3This is an example QMK CLI script.
4"""
5from milc import cli
6
7
8@cli.argument('-n', '--name', default='World', help='Name to greet.')
9@cli.entrypoint('QMK Hello World.')
10def main(cli):
11 """Log a friendly greeting.
12 """
13 cli.log.info('Hello, %s!', cli.config.general.name)
diff --git a/lib/python/qmk/cli/json/__init__.py b/lib/python/qmk/cli/json/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/lib/python/qmk/cli/json/__init__.py
diff --git a/lib/python/qmk/cli/json/keymap.py b/lib/python/qmk/cli/json/keymap.py
new file mode 100755
index 000000000..35fc8f9c0
--- /dev/null
+++ b/lib/python/qmk/cli/json/keymap.py
@@ -0,0 +1,54 @@
1"""Generate a keymap.c from a configurator export.
2"""
3import json
4import os
5import sys
6
7from milc import cli
8
9import qmk.keymap
10
11
12@cli.argument('-o', '--output', help='File to write to')
13@cli.argument('filename', help='Configurator JSON file')
14@cli.entrypoint('Create a keymap.c from a QMK Configurator export.')
15def main(cli):
16 """Generate a keymap.c from a configurator export.
17
18 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.
19 """
20 # Error checking
21 if cli.args.filename == ('-'):
22 cli.log.error('Reading from STDIN is not (yet) supported.')
23 cli.print_usage()
24 exit(1)
25 if not os.path.exists(qmk.path.normpath(cli.args.filename)):
26 cli.log.error('JSON file does not exist!')
27 cli.print_usage()
28 exit(1)
29
30 # Environment processing
31 if cli.args.output == ('-'):
32 cli.args.output = None
33
34 # Parse the configurator json
35 with open(qmk.path.normpath(cli.args.filename), 'r') as fd:
36 user_keymap = json.load(fd)
37
38 # Generate the keymap
39 keymap_c = qmk.keymap.generate(user_keymap['keyboard'], user_keymap['layout'], user_keymap['layers'])
40
41 if cli.args.output:
42 output_dir = os.path.dirname(cli.args.output)
43
44 if not os.path.exists(output_dir):
45 os.makedirs(output_dir)
46
47 output_file = qmk.path.normpath(cli.args.output)
48 with open(output_file, 'w') as keymap_fd:
49 keymap_fd.write(keymap_c)
50
51 cli.log.info('Wrote keymap to %s.', cli.args.output)
52
53 else:
54 print(keymap_c)
diff --git a/lib/python/qmk/errors.py b/lib/python/qmk/errors.py
new file mode 100644
index 000000000..f9bf5b9af
--- /dev/null
+++ b/lib/python/qmk/errors.py
@@ -0,0 +1,6 @@
1class NoSuchKeyboardError(Exception):
2 """Raised when we can't find a keyboard/keymap directory.
3 """
4
5 def __init__(self, message):
6 self.message = message
diff --git a/lib/python/qmk/keymap.py b/lib/python/qmk/keymap.py
new file mode 100644
index 000000000..6eccab788
--- /dev/null
+++ b/lib/python/qmk/keymap.py
@@ -0,0 +1,100 @@
1"""Functions that help you work with QMK keymaps.
2"""
3import json
4import logging
5import os
6from traceback import format_exc
7
8import qmk.path
9from qmk.errors import NoSuchKeyboardError
10
11# The `keymap.c` template to use when a keyboard doesn't have its own
12DEFAULT_KEYMAP_C = """#include QMK_KEYBOARD_H
13
14/* THIS FILE WAS GENERATED!
15 *
16 * This file was generated by qmk-compile-json. You may or may not want to
17 * edit it directly.
18 */
19
20const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
21__KEYMAP_GOES_HERE__
22};
23"""
24
25
26def template(keyboard):
27 """Returns the `keymap.c` template for a keyboard.
28
29 If a template exists in `keyboards/<keyboard>/templates/keymap.c` that
30 text will be used instead of `DEFAULT_KEYMAP_C`.
31
32 Args:
33 keyboard
34 The keyboard to return a template for.
35 """
36 template_name = 'keyboards/%s/templates/keymap.c' % keyboard
37
38 if os.path.exists(template_name):
39 with open(template_name, 'r') as fd:
40 return fd.read()
41
42 return DEFAULT_KEYMAP_C
43
44
45def generate(keyboard, layout, layers):
46 """Returns a keymap.c for the specified keyboard, layout, and layers.
47
48 Args:
49 keyboard
50 The name of the keyboard
51
52 layout
53 The LAYOUT macro this keymap uses.
54
55 layers
56 An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode.
57 """
58 layer_txt = []
59 for layer_num, layer in enumerate(layers):
60 if layer_num != 0:
61 layer_txt[-1] = layer_txt[-1] + ','
62 layer_keys = ', '.join(layer)
63 layer_txt.append('\t[%s] = %s(%s)' % (layer_num, layout, layer_keys))
64
65 keymap = '\n'.join(layer_txt)
66 keymap_c = template(keyboard, keymap)
67
68 return keymap_c.replace('__KEYMAP_GOES_HERE__', keymap)
69
70
71def write(keyboard, keymap, layout, layers):
72 """Generate the `keymap.c` and write it to disk.
73
74 Returns the filename written to.
75
76 Args:
77 keyboard
78 The name of the keyboard
79
80 keymap
81 The name of the keymap
82
83 layout
84 The LAYOUT macro this keymap uses.
85
86 layers
87 An array of arrays describing the keymap. Each item in the inner array should be a string that is a valid QMK keycode.
88 """
89 keymap_c = generate(keyboard, layout, layers)
90 keymap_path = qmk.path.keymap(keyboard)
91 keymap_dir = os.path.join(keymap_path, keymap)
92 keymap_file = os.path.join(keymap_dir, 'keymap.c')
93
94 if not os.path.exists(keymap_dir):
95 os.makedirs(keymap_dir)
96
97 with open(keymap_file, 'w') as keymap_fd:
98 keymap_fd.write(keymap_c)
99
100 return keymap_file
diff --git a/lib/python/qmk/path.py b/lib/python/qmk/path.py
new file mode 100644
index 000000000..f2a8346a5
--- /dev/null
+++ b/lib/python/qmk/path.py
@@ -0,0 +1,32 @@
1"""Functions that help us work with files and folders.
2"""
3import os
4
5
6def keymap(keyboard):
7 """Locate the correct directory for storing a keymap.
8
9 Args:
10 keyboard
11 The name of the keyboard. Example: clueboard/66/rev3
12 """
13 for directory in ['.', '..', '../..', '../../..', '../../../..', '../../../../..']:
14 basepath = os.path.normpath(os.path.join('keyboards', keyboard, directory, 'keymaps'))
15
16 if os.path.exists(basepath):
17 return basepath
18
19 logging.error('Could not find keymaps directory!')
20 raise NoSuchKeyboardError('Could not find keymaps directory for: %s' % keyboard)
21
22
23def normpath(path):
24 """Returns the fully resolved absolute path to a file.
25
26 This function will return the absolute path to a file as seen from the
27 directory the script was called from.
28 """
29 if path and path[0] == '/':
30 return os.path.normpath(path)
31
32 return os.path.normpath(os.path.join(os.environ['ORIG_CWD'], path))
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 000000000..351dc2524
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,5 @@
1# Python requirements
2# milc FIXME(skullydazed): Included in the repo for now.
3argcomplete
4colorama
5#halo
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 000000000..528512ac6
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,330 @@
1# Python settings for QMK
2
3[yapf]
4# Align closing bracket with visual indentation.
5align_closing_bracket_with_visual_indent=True
6
7# Allow dictionary keys to exist on multiple lines. For example:
8#
9# x = {
10# ('this is the first element of a tuple',
11# 'this is the second element of a tuple'):
12# value,
13# }
14allow_multiline_dictionary_keys=False
15
16# Allow lambdas to be formatted on more than one line.
17allow_multiline_lambdas=False
18
19# Allow splitting before a default / named assignment in an argument list.
20allow_split_before_default_or_named_assigns=True
21
22# Allow splits before the dictionary value.
23allow_split_before_dict_value=True
24
25# Let spacing indicate operator precedence. For example:
26#
27# a = 1 * 2 + 3 / 4
28# b = 1 / 2 - 3 * 4
29# c = (1 + 2) * (3 - 4)
30# d = (1 - 2) / (3 + 4)
31# e = 1 * 2 - 3
32# f = 1 + 2 + 3 + 4
33#
34# will be formatted as follows to indicate precedence:
35#
36# a = 1*2 + 3/4
37# b = 1/2 - 3*4
38# c = (1+2) * (3-4)
39# d = (1-2) / (3+4)
40# e = 1*2 - 3
41# f = 1 + 2 + 3 + 4
42#
43arithmetic_precedence_indication=True
44
45# Number of blank lines surrounding top-level function and class
46# definitions.
47blank_lines_around_top_level_definition=2
48
49# Insert a blank line before a class-level docstring.
50blank_line_before_class_docstring=False
51
52# Insert a blank line before a module docstring.
53blank_line_before_module_docstring=False
54
55# Insert a blank line before a 'def' or 'class' immediately nested
56# within another 'def' or 'class'. For example:
57#
58# class Foo:
59# # <------ this blank line
60# def method():
61# ...
62blank_line_before_nested_class_or_def=False
63
64# Do not split consecutive brackets. Only relevant when
65# dedent_closing_brackets is set. For example:
66#
67# call_func_that_takes_a_dict(
68# {
69# 'key1': 'value1',
70# 'key2': 'value2',
71# }
72# )
73#
74# would reformat to:
75#
76# call_func_that_takes_a_dict({
77# 'key1': 'value1',
78# 'key2': 'value2',
79# })
80coalesce_brackets=True
81
82# The column limit.
83column_limit=256
84
85# The style for continuation alignment. Possible values are:
86#
87# - SPACE: Use spaces for continuation alignment. This is default behavior.
88# - FIXED: Use fixed number (CONTINUATION_INDENT_WIDTH) of columns
89# (ie: CONTINUATION_INDENT_WIDTH/INDENT_WIDTH tabs) for continuation
90# alignment.
91# - VALIGN-RIGHT: Vertically align continuation lines with indent
92# characters. Slightly right (one more indent character) if cannot
93# vertically align continuation lines with indent characters.
94#
95# For options FIXED, and VALIGN-RIGHT are only available when USE_TABS is
96# enabled.
97continuation_align_style=SPACE
98
99# Indent width used for line continuations.
100continuation_indent_width=4
101
102# Put closing brackets on a separate line, dedented, if the bracketed
103# expression can't fit in a single line. Applies to all kinds of brackets,
104# including function definitions and calls. For example:
105#
106# config = {
107# 'key1': 'value1',
108# 'key2': 'value2',
109# } # <--- this bracket is dedented and on a separate line
110#
111# time_series = self.remote_client.query_entity_counters(
112# entity='dev3246.region1',
113# key='dns.query_latency_tcp',
114# transform=Transformation.AVERAGE(window=timedelta(seconds=60)),
115# start_ts=now()-timedelta(days=3),
116# end_ts=now(),
117# ) # <--- this bracket is dedented and on a separate line
118dedent_closing_brackets=True
119
120# Disable the heuristic which places each list element on a separate line
121# if the list is comma-terminated.
122disable_ending_comma_heuristic=False
123
124# Place each dictionary entry onto its own line.
125each_dict_entry_on_separate_line=True
126
127# The regex for an i18n comment. The presence of this comment stops
128# reformatting of that line, because the comments are required to be
129# next to the string they translate.
130i18n_comment=
131
132# The i18n function call names. The presence of this function stops
133# reformattting on that line, because the string it has cannot be moved
134# away from the i18n comment.
135i18n_function_call=
136
137# Indent blank lines.
138indent_blank_lines=False
139
140# Indent the dictionary value if it cannot fit on the same line as the
141# dictionary key. For example:
142#
143# config = {
144# 'key1':
145# 'value1',
146# 'key2': value1 +
147# value2,
148# }
149indent_dictionary_value=True
150
151# The number of columns to use for indentation.
152indent_width=4
153
154# Join short lines into one line. E.g., single line 'if' statements.
155join_multiple_lines=False
156
157# Do not include spaces around selected binary operators. For example:
158#
159# 1 + 2 * 3 - 4 / 5
160#
161# will be formatted as follows when configured with "*,/":
162#
163# 1 + 2*3 - 4/5
164no_spaces_around_selected_binary_operators=
165
166# Use spaces around default or named assigns.
167spaces_around_default_or_named_assign=False
168
169# Use spaces around the power operator.
170spaces_around_power_operator=False
171
172# The number of spaces required before a trailing comment.
173# This can be a single value (representing the number of spaces
174# before each trailing comment) or list of values (representing
175# alignment column values; trailing comments within a block will
176# be aligned to the first column value that is greater than the maximum
177# line length within the block). For example:
178#
179# With spaces_before_comment=5:
180#
181# 1 + 1 # Adding values
182#
183# will be formatted as:
184#
185# 1 + 1 # Adding values <-- 5 spaces between the end of the statement and comment
186#
187# With spaces_before_comment=15, 20:
188#
189# 1 + 1 # Adding values
190# two + two # More adding
191#
192# longer_statement # This is a longer statement
193# short # This is a shorter statement
194#
195# a_very_long_statement_that_extends_beyond_the_final_column # Comment
196# short # This is a shorter statement
197#
198# will be formatted as:
199#
200# 1 + 1 # Adding values <-- end of line comments in block aligned to col 15
201# two + two # More adding
202#
203# longer_statement # This is a longer statement <-- end of line comments in block aligned to col 20
204# short # This is a shorter statement
205#
206# a_very_long_statement_that_extends_beyond_the_final_column # Comment <-- the end of line comments are aligned based on the line length
207# short # This is a shorter statement
208#
209spaces_before_comment=2
210
211# Insert a space between the ending comma and closing bracket of a list,
212# etc.
213space_between_ending_comma_and_closing_bracket=False
214
215# Split before arguments
216split_all_comma_separated_values=False
217
218# Split before arguments if the argument list is terminated by a
219# comma.
220split_arguments_when_comma_terminated=True
221
222# Set to True to prefer splitting before '+', '-', '*', '/', '//', or '@'
223# rather than after.
224split_before_arithmetic_operator=False
225
226# Set to True to prefer splitting before '&', '|' or '^' rather than
227# after.
228split_before_bitwise_operator=True
229
230# Split before the closing bracket if a list or dict literal doesn't fit on
231# a single line.
232split_before_closing_bracket=True
233
234# Split before a dictionary or set generator (comp_for). For example, note
235# the split before the 'for':
236#
237# foo = {
238# variable: 'Hello world, have a nice day!'
239# for variable in bar if variable != 42
240# }
241split_before_dict_set_generator=True
242
243# Split before the '.' if we need to split a longer expression:
244#
245# foo = ('This is a really long string: {}, {}, {}, {}'.format(a, b, c, d))
246#
247# would reformat to something like:
248#
249# foo = ('This is a really long string: {}, {}, {}, {}'
250# .format(a, b, c, d))
251split_before_dot=False
252
253# Split after the opening paren which surrounds an expression if it doesn't
254# fit on a single line.
255split_before_expression_after_opening_paren=False
256
257# If an argument / parameter list is going to be split, then split before
258# the first argument.
259split_before_first_argument=False
260
261# Set to True to prefer splitting before 'and' or 'or' rather than
262# after.
263split_before_logical_operator=False
264
265# Split named assignments onto individual lines.
266split_before_named_assigns=True
267
268# Set to True to split list comprehensions and generators that have
269# non-trivial expressions and multiple clauses before each of these
270# clauses. For example:
271#
272# result = [
273# a_long_var + 100 for a_long_var in xrange(1000)
274# if a_long_var % 10]
275#
276# would reformat to something like:
277#
278# result = [
279# a_long_var + 100
280# for a_long_var in xrange(1000)
281# if a_long_var % 10]
282split_complex_comprehension=True
283
284# The penalty for splitting right after the opening bracket.
285split_penalty_after_opening_bracket=300
286
287# The penalty for splitting the line after a unary operator.
288split_penalty_after_unary_operator=10000
289
290# The penalty of splitting the line around the '+', '-', '*', '/', '//',
291# ``%``, and '@' operators.
292split_penalty_arithmetic_operator=300
293
294# The penalty for splitting right before an if expression.
295split_penalty_before_if_expr=0
296
297# The penalty of splitting the line around the '&', '|', and '^'
298# operators.
299split_penalty_bitwise_operator=300
300
301# The penalty for splitting a list comprehension or generator
302# expression.
303split_penalty_comprehension=80
304
305# The penalty for characters over the column limit.
306split_penalty_excess_character=7000
307
308# The penalty incurred by adding a line split to the unwrapped line. The
309# more line splits added the higher the penalty.
310split_penalty_for_added_line_split=30
311
312# The penalty of splitting a list of "import as" names. For example:
313#
314# from a_very_long_or_indented_module_name_yada_yad import (long_argument_1,
315# long_argument_2,
316# long_argument_3)
317#
318# would reformat to something like:
319#
320# from a_very_long_or_indented_module_name_yada_yad import (
321# long_argument_1, long_argument_2, long_argument_3)
322split_penalty_import_names=0
323
324# The penalty of splitting the line around the 'and' and 'or'
325# operators.
326split_penalty_logical_operator=300
327
328# Use the Tab character for indentation.
329use_tabs=False
330
diff --git a/util/freebsd_install.sh b/util/freebsd_install.sh
index c8696e8cc..815759203 100755
--- a/util/freebsd_install.sh
+++ b/util/freebsd_install.sh
@@ -1,4 +1,5 @@
1#!/bin/sh 1#!/bin/sh
2util_dir=$(dirname "$0")
2pkg update 3pkg update
3pkg install -y \ 4pkg install -y \
4 git \ 5 git \
@@ -17,3 +18,4 @@ pkg install -y \
17 arm-none-eabi-newlib \ 18 arm-none-eabi-newlib \
18 diffutils \ 19 diffutils \
19 python3 20 python3
21pip3 install -r ${util_dir}/../requirements.txt
diff --git a/util/linux_install.sh b/util/linux_install.sh
index 4731ec015..d21cd3c1c 100755
--- a/util/linux_install.sh
+++ b/util/linux_install.sh
@@ -8,6 +8,8 @@ SLACKWARE_WARNING="You will need the following packages from slackbuilds.org:\n\
8 8
9SOLUS_INFO="Your tools are now installed. To start using them, open new terminal or source these scripts:\n\t/usr/share/defaults/etc/profile.d/50-arm-toolchain-path.sh\n\t/usr/share/defaults/etc/profile.d/50-avr-toolchain-path.sh" 9SOLUS_INFO="Your tools are now installed. To start using them, open new terminal or source these scripts:\n\t/usr/share/defaults/etc/profile.d/50-arm-toolchain-path.sh\n\t/usr/share/defaults/etc/profile.d/50-avr-toolchain-path.sh"
10 10
11util_dir=$(dirname "$0")
12
11if grep ID /etc/os-release | grep -qE "fedora"; then 13if grep ID /etc/os-release | grep -qE "fedora"; then
12 sudo dnf install \ 14 sudo dnf install \
13 arm-none-eabi-binutils-cs \ 15 arm-none-eabi-binutils-cs \
@@ -183,3 +185,6 @@ else
183 echo 185 echo
184 echo "https://docs.qmk.fm/#/contributing" 186 echo "https://docs.qmk.fm/#/contributing"
185fi 187fi
188
189# Global install tasks
190pip3 install -r ${util_dir}/../requirements.txt
diff --git a/util/macos_install.sh b/util/macos_install.sh
index 915ff3143..f7e304424 100755
--- a/util/macos_install.sh
+++ b/util/macos_install.sh
@@ -1,5 +1,7 @@
1#!/bin/bash 1#!/bin/bash
2 2
3util_dir=$(dirname "$0")
4
3if ! brew --version 2>&1 > /dev/null; then 5if ! brew --version 2>&1 > /dev/null; then
4 echo "Error! Homebrew not installed or broken!" 6 echo "Error! Homebrew not installed or broken!"
5 echo -n "Would you like to install homebrew now? [y/n] " 7 echo -n "Would you like to install homebrew now? [y/n] "
@@ -24,3 +26,4 @@ brew tap PX4/homebrew-px4
24brew update 26brew update
25brew install avr-gcc@8 gcc-arm-none-eabi dfu-programmer avrdude dfu-util python3 27brew install avr-gcc@8 gcc-arm-none-eabi dfu-programmer avrdude dfu-util python3
26brew link --force avr-gcc@8 28brew link --force avr-gcc@8
29pip3 install -r ${util_dir}/../requirements.txt
diff --git a/util/msys2_install.sh b/util/msys2_install.sh
index bcb628ab2..bed176da6 100755
--- a/util/msys2_install.sh
+++ b/util/msys2_install.sh
@@ -5,6 +5,7 @@ download_dir=~/qmk_utils
5avrtools=avr8-gnu-toolchain 5avrtools=avr8-gnu-toolchain
6armtools=gcc-arm-none-eabi 6armtools=gcc-arm-none-eabi
7installflip=false 7installflip=false
8util_dir=$(dirname "$0")
8 9
9echo "Installing dependencies needed for the installation (quazip)" 10echo "Installing dependencies needed for the installation (quazip)"
10pacman --needed -S base-devel mingw-w64-x86_64-toolchain msys/git msys/p7zip msys/python3 msys/unzip 11pacman --needed -S base-devel mingw-w64-x86_64-toolchain msys/git msys/p7zip msys/python3 msys/unzip
@@ -92,6 +93,8 @@ else
92fi 93fi
93popd 94popd
94 95
96pip3 install -r ${util_dir}/../requirements.txt
97
95cp -f "$dir/activate_msys2.sh" "$download_dir/" 98cp -f "$dir/activate_msys2.sh" "$download_dir/"
96 99
97if grep "^source ~/qmk_utils/activate_msys2.sh$" ~/.bashrc 100if grep "^source ~/qmk_utils/activate_msys2.sh$" ~/.bashrc
diff --git a/util/wsl_install.sh b/util/wsl_install.sh
index c2c206d2b..197d9f089 100755
--- a/util/wsl_install.sh
+++ b/util/wsl_install.sh
@@ -1,6 +1,7 @@
1#!/bin/bash 1#!/bin/bash
2 2
3dir=$(cd -P -- "$(dirname -- "$0")" && pwd -P) 3util_dir=$(dirname "$0")
4dir=$(cd -P -- "$util_dir" && pwd -P)
4pushd "$dir"; 5pushd "$dir";
5 6
6if [[ $dir != /mnt/* ]]; 7if [[ $dir != /mnt/* ]];
@@ -28,6 +29,8 @@ download_dir=wsl_downloaded
28 29
29source "$dir/win_shared_install.sh" 30source "$dir/win_shared_install.sh"
30 31
32pip3 install -r ${util_dir}/../requirements.txt
33
31pushd "$download_dir" 34pushd "$download_dir"
32while true; do 35while true; do
33 echo 36 echo