Skip to content

Commit

Permalink
Add decorators for determining keyboard and keymap based on current d…
Browse files Browse the repository at this point in the history
…irectory (qmk#8191)

* Use pathlib everywhere we can

* Improvements based on @Erovia's feedback

* rework qmk compile and qmk flash to use pathlib

* style

* Remove the subcommand_name argument from find_keyboard_keymap()

* add experimental decorators

* Create decorators for finding keyboard and keymap based on current directory.

Decorators were inspired by @Erovia's brilliant work on the proof of concept.
  • Loading branch information
skullydazed committed Mar 13, 2020
1 parent 5e98eaa commit f81b0e3
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 99 deletions.
5 changes: 5 additions & 0 deletions lib/python/qmk/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
We list each subcommand here explicitly because all the reliable ways of searching for modules are slow and delay startup.
"""
from milc import cli

from . import cformat
from . import compile
from . import config
Expand All @@ -16,3 +18,6 @@
from . import new
from . import pyformat
from . import pytest

if not hasattr(cli, 'config_source'):
cli.log.warning("Your QMK CLI is out of date. Please upgrade with `pip3 install --upgrade qmk` or by using your package manager.")
37 changes: 25 additions & 12 deletions lib/python/qmk/cli/compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,28 @@
from milc import cli

import qmk.path
from qmk.commands import compile_configurator_json, create_make_command, find_keyboard_keymap, parse_configurator_json
from qmk.decorators import automagic_keyboard, automagic_keymap
from qmk.commands import compile_configurator_json, create_make_command, parse_configurator_json


@cli.argument('filename', nargs='?', arg_only=True, type=FileType('r'), help='The configurator export to compile')
@cli.argument('-kb', '--keyboard', help='The keyboard to build a firmware for. Ignored when a configurator export is supplied.')
@cli.argument('-km', '--keymap', help='The keymap to build a firmware for. Ignored when a configurator export is supplied.')
@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually build, just show the make command to be run.")
@cli.subcommand('Compile a QMK Firmware.')
@automagic_keyboard
@automagic_keymap
def compile(cli):
"""Compile a QMK Firmware.
If a Configurator export is supplied this command will create a new keymap, overwriting an existing keymap if one exists.
If a keyboard and keymap are provided this command will build a firmware based on that.
"""
command = None

if cli.args.filename:
# If a configurator JSON was provided skip straight to compiling it
# If a configurator JSON was provided generate a keymap and compile it
# FIXME(skullydazed): add code to check and warn if the keymap already exists when compiling a json keymap.
user_keymap = parse_configurator_json(cli.args.filename)
keymap_path = qmk.path.keymap(user_keymap['keyboard'])
Expand All @@ -32,16 +38,23 @@ def compile(cli):
cli.log.info('Wrote keymap to {fg_cyan}%s/%s/keymap.c', keymap_path, user_keymap['keymap'])

else:
# Perform the action the user specified
user_keyboard, user_keymap = find_keyboard_keymap()
if user_keyboard and user_keymap:
if cli.config.compile.keyboard and cli.config.compile.keymap:
# Generate the make command for a specific keyboard/keymap.
command = create_make_command(user_keyboard, user_keymap)
command = create_make_command(cli.config.compile.keyboard, cli.config.compile.keymap)

elif not cli.config.compile.keyboard:
cli.log.error('Could not determine keyboard!')
elif not cli.config.compile.keymap:
cli.log.error('Could not determine keymap!')

else:
cli.log.error('You must supply a configurator export, both `--keyboard` and `--keymap`, or be in a directory for a keyboard or keymap.')
cli.echo('usage: qmk compile [-h] [-b] [-kb KEYBOARD] [-km KEYMAP] [filename]')
return False
# Compile the firmware, if we're able to
if command:
cli.log.info('Compiling keymap with {fg_cyan}%s', ' '.join(command))
if not cli.args.dry_run:
cli.echo('\n')
subprocess.run(command)

cli.log.info('Compiling keymap with {fg_cyan}%s\n\n', ' '.join(command))
subprocess.run(command)
else:
cli.log.error('You must supply a configurator export, both `--keyboard` and `--keymap`, or be in a directory for a keyboard or keymap.')
cli.echo('usage: qmk compile [-h] [-b] [-kb KEYBOARD] [-km KEYMAP] [filename]')
return False
47 changes: 30 additions & 17 deletions lib/python/qmk/cli/flash.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
import subprocess
from argparse import FileType

import qmk.path
from milc import cli
from qmk.commands import compile_configurator_json, create_make_command, find_keyboard_keymap, parse_configurator_json

import qmk.path
from qmk.decorators import automagic_keyboard, automagic_keymap
from qmk.commands import compile_configurator_json, create_make_command, parse_configurator_json


def print_bootloader_help():
Expand All @@ -28,12 +30,15 @@ def print_bootloader_help():
cli.echo('For more info, visit https://docs.qmk.fm/#/flashing')


@cli.argument('-bl', '--bootloader', default='flash', help='The flash command, corresponding to qmk\'s make options of bootloaders.')
@cli.argument('filename', nargs='?', arg_only=True, type=FileType('r'), help='The configurator export JSON to compile.')
@cli.argument('-b', '--bootloaders', action='store_true', help='List the available bootloaders.')
@cli.argument('-bl', '--bootloader', default='flash', help='The flash command, corresponding to qmk\'s make options of bootloaders.')
@cli.argument('-km', '--keymap', help='The keymap to build a firmware for. Use this if you dont have a configurator file. Ignored when a configurator file is supplied.')
@cli.argument('-kb', '--keyboard', help='The keyboard to build a firmware for. Use this if you dont have a configurator file. Ignored when a configurator file is supplied.')
@cli.argument('-b', '--bootloaders', action='store_true', help='List the available bootloaders.')
@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually build, just show the make command to be run.")
@cli.subcommand('QMK Flash.')
@automagic_keyboard
@automagic_keymap
def flash(cli):
"""Compile and or flash QMK Firmware or keyboard/layout
Expand All @@ -42,12 +47,13 @@ def flash(cli):
If no file is supplied, keymap and keyboard are expected.
If bootloader is omitted, the one according to the rules.mk will be used.
If bootloader is omitted the make system will use the configured bootloader for that keyboard.
"""
command = ''

if cli.args.bootloaders:
# Provide usage and list bootloaders
cli.echo('usage: qmk flash [-h] [-b] [-kb KEYBOARD] [-km KEYMAP] [-bl BOOTLOADER] [filename]')
cli.echo('usage: qmk flash [-h] [-b] [-n] [-kb KEYBOARD] [-km KEYMAP] [-bl BOOTLOADER] [filename]')
print_bootloader_help()
return False

Expand All @@ -60,16 +66,23 @@ def flash(cli):
cli.log.info('Wrote keymap to {fg_cyan}%s/%s/keymap.c', keymap_path, user_keymap['keymap'])

else:
# Perform the action the user specified
user_keyboard, user_keymap = find_keyboard_keymap()
if user_keyboard and user_keymap:
if cli.config.flash.keyboard and cli.config.flash.keymap:
# Generate the make command for a specific keyboard/keymap.
command = create_make_command(user_keyboard, user_keymap, cli.args.bootloader)
command = create_make_command(cli.config.flash.keyboard, cli.config.flash.keymap, cli.args.bootloader)

else:
cli.log.error('You must supply a configurator export or both `--keyboard` and `--keymap`.')
cli.echo('usage: qmk flash [-h] [-b] [-kb KEYBOARD] [-km KEYMAP] [-bl BOOTLOADER] [filename]')
return False
elif not cli.config.flash.keyboard:
cli.log.error('Could not determine keyboard!')
elif not cli.config.flash.keymap:
cli.log.error('Could not determine keymap!')

cli.log.info('Flashing keymap with {fg_cyan}%s\n\n', ' '.join(command))
subprocess.run(command)
# Compile the firmware, if we're able to
if command:
cli.log.info('Compiling keymap with {fg_cyan}%s', ' '.join(command))
if not cli.args.dry_run:
cli.echo('\n')
subprocess.run(command)

else:
cli.log.error('You must supply a configurator export, both `--keyboard` and `--keymap`, or be in a directory for a keyboard or keymap.')
cli.echo('usage: qmk flash [-h] [-b] [-n] [-kb KEYBOARD] [-km KEYMAP] [-bl BOOTLOADER] [filename]')
return False
3 changes: 3 additions & 0 deletions lib/python/qmk/cli/list/keymaps.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
"""List the keymaps for a specific keyboard
"""
from milc import cli

import qmk.keymap
from qmk.decorators import automagic_keyboard
from qmk.errors import NoSuchKeyboardError


@cli.argument("-kb", "--keyboard", help="Specify keyboard name. Example: 1upkeyboards/1up60hse")
@cli.subcommand("List the keymaps for a specific keyboard")
@automagic_keyboard
def list_keymaps(cli):
"""List the keymaps for a specific keyboard
"""
Expand Down
3 changes: 3 additions & 0 deletions lib/python/qmk/cli/new/keymap.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
from pathlib import Path

import qmk.path
from qmk.decorators import automagic_keyboard, automagic_keymap
from milc import cli


@cli.argument('-kb', '--keyboard', help='Specify keyboard name. Example: 1upkeyboards/1up60hse')
@cli.argument('-km', '--keymap', help='Specify the name for the new keymap directory')
@cli.subcommand('Creates a new keymap for the keyboard of your choosing')
@automagic_keyboard
@automagic_keymap
def new_keymap(cli):
"""Creates a new keymap for the keyboard of your choosing.
"""
Expand Down
69 changes: 0 additions & 69 deletions lib/python/qmk/commands.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
"""Helper functions for commands.
"""
import json
from pathlib import Path

from milc import cli

import qmk.keymap
from qmk.path import is_keyboard, is_keymap_dir, under_qmk_firmware


def create_make_command(keyboard, keymap, target=None):
Expand Down Expand Up @@ -59,71 +55,6 @@ def compile_configurator_json(user_keymap, bootloader=None):
return create_make_command(user_keymap['keyboard'], user_keymap['keymap'], bootloader)


def find_keyboard_keymap():
"""Returns `(keyboard_name, keymap_name)` based on the user's current environment.
This determines the keyboard and keymap name using the following precedence order:
* Command line flags (--keyboard and --keymap)
* Current working directory
* `keyboards/<keyboard_name>`
* `keyboards/<keyboard_name>/keymaps/<keymap_name>`
* `layouts/**/<keymap_name>`
* `users/<keymap_name>`
* Configuration
* cli.config.<subcommand>.keyboard
* cli.config.<subcommand>.keymap
"""
# Check to make sure their copy of MILC supports config_source
if not hasattr(cli, 'config_source'):
cli.log.error("Your QMK CLI is out of date. Please upgrade using pip3 or your package manager.")
exit(1)

# State variables
relative_cwd = under_qmk_firmware()
keyboard_name = ""
keymap_name = ""

# If the keyboard or keymap are passed as arguments use that in preference to anything else
if cli.config_source[cli._entrypoint.__name__]['keyboard'] == 'argument':
keyboard_name = cli.config[cli._entrypoint.__name__]['keyboard']
if cli.config_source[cli._entrypoint.__name__]['keymap'] == 'argument':
keymap_name = cli.config[cli._entrypoint.__name__]['keymap']

if not keyboard_name or not keymap_name:
# If we don't have a keyboard_name and keymap_name from arguments try to derive one or both
if relative_cwd and relative_cwd.parts and relative_cwd.parts[0] == 'keyboards':
# Try to determine the keyboard and/or keymap name
current_path = Path('/'.join(relative_cwd.parts[1:]))

if current_path.parts[-2] == 'keymaps':
if not keymap_name:
keymap_name = current_path.parts[-1]
if not keyboard_name:
keyboard_name = '/'.join(current_path.parts[:-2])
elif not keyboard_name and is_keyboard(current_path):
keyboard_name = str(current_path)

elif relative_cwd and relative_cwd.parts and relative_cwd.parts[0] == 'layouts':
# Try to determine the keymap name from the community layout
if is_keymap_dir(relative_cwd) and not keymap_name:
keymap_name = relative_cwd.name

elif relative_cwd and relative_cwd.parts and relative_cwd.parts[0] == 'users':
# Try to determine the keymap name based on which userspace they're in
if not keymap_name and len(relative_cwd.parts) > 1:
keymap_name = relative_cwd.parts[1]

# If we still don't have a keyboard and keymap check the config
if not keyboard_name and cli.config[cli._entrypoint.__name__]['keyboard']:
keyboard_name = cli.config[cli._entrypoint.__name__]['keyboard']

if not keymap_name and cli.config[cli._entrypoint.__name__]['keymap']:
keymap_name = cli.config[cli._entrypoint.__name__]['keymap']

return (keyboard_name, keymap_name)


def parse_configurator_json(configurator_file):
"""Open and parse a configurator json export
"""
Expand Down
85 changes: 85 additions & 0 deletions lib/python/qmk/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Helpful decorators that subcommands can use.
"""
import functools
from pathlib import Path

from milc import cli

from qmk.path import is_keyboard, is_keymap_dir, under_qmk_firmware


def automagic_keyboard(func):
"""Sets `cli.config.<subcommand>.keyboard` based on environment.
This will rewrite cli.config.<subcommand>.keyboard if the user did not pass `--keyboard` and the directory they are currently in is a keyboard or keymap directory.
"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Check to make sure their copy of MILC supports config_source
if not hasattr(cli, 'config_source'):
cli.log.error("This subcommand requires a newer version of the QMK CLI. Please upgrade using `pip3 install --upgrade qmk` or your package manager.")
exit(1)

# Ensure that `--keyboard` was not passed and CWD is under `qmk_firmware/keyboards`
if cli.config_source[cli._entrypoint.__name__]['keyboard'] != 'argument':
relative_cwd = under_qmk_firmware()

if relative_cwd and len(relative_cwd.parts) > 1 and relative_cwd.parts[0] == 'keyboards':
# Attempt to extract the keyboard name from the current directory
current_path = Path('/'.join(relative_cwd.parts[1:]))

if 'keymaps' in current_path.parts:
# Strip current_path of anything after `keymaps`
keymap_index = len(current_path.parts) - current_path.parts.index('keymaps') - 1
current_path = current_path.parents[keymap_index]

if is_keyboard(current_path):
cli.config[cli._entrypoint.__name__]['keyboard'] = str(current_path)
cli.config_source[cli._entrypoint.__name__]['keyboard'] = 'keyboard_directory'

return func(*args, **kwargs)

return wrapper


def automagic_keymap(func):
"""Sets `cli.config.<subcommand>.keymap` based on environment.
This will rewrite cli.config.<subcommand>.keymap if the user did not pass `--keymap` and the directory they are currently in is a keymap, layout, or user directory.
"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Check to make sure their copy of MILC supports config_source
if not hasattr(cli, 'config_source'):
cli.log.error("This subcommand requires a newer version of the QMK CLI. Please upgrade using `pip3 install --upgrade qmk` or your package manager.")
exit(1)

# Ensure that `--keymap` was not passed and that we're under `qmk_firmware`
if cli.config_source[cli._entrypoint.__name__]['keymap'] != 'argument':
relative_cwd = under_qmk_firmware()

if relative_cwd and len(relative_cwd.parts) > 1:
# If we're in `qmk_firmware/keyboards` and `keymaps` is in our path, try to find the keyboard name.
if relative_cwd.parts[0] == 'keyboards' and 'keymaps' in relative_cwd.parts:
current_path = Path('/'.join(relative_cwd.parts[1:])) # Strip 'keyboards' from the front

if 'keymaps' in current_path.parts and current_path.name != 'keymaps':
while current_path.parent.name != 'keymaps':
current_path = current_path.parent
cli.config[cli._entrypoint.__name__]['keymap'] = current_path.name
cli.config_source[cli._entrypoint.__name__]['keyboard'] = 'keymap_directory'

# If we're in `qmk_firmware/layouts` guess the name from the community keymap they're in
elif relative_cwd.parts[0] == 'layouts' and is_keymap_dir(relative_cwd):
cli.config[cli._entrypoint.__name__]['keymap'] = relative_cwd.name
cli.config_source[cli._entrypoint.__name__]['keyboard'] = 'layouts_directory'

# If we're in `qmk_firmware/users` guess the name from the userspace they're in
elif relative_cwd.parts[0] == 'users':
# Guess the keymap name based on which userspace they're in
cli.config[cli._entrypoint.__name__]['keymap'] = relative_cwd.parts[1]
cli.config_source[cli._entrypoint.__name__]['keyboard'] = 'users_directory'

return func(*args, **kwargs)

return wrapper
2 changes: 1 addition & 1 deletion lib/python/qmk/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def normpath(path):
path = Path(path)

if path.is_absolute():
return Path(path)
return path

return Path(os.environ['ORIG_CWD']) / path

Expand Down

0 comments on commit f81b0e3

Please sign in to comment.