From 588bcdc8ca212b195a428fc43766a59a9252c08d Mon Sep 17 00:00:00 2001 From: Zach White Date: Wed, 14 Apr 2021 19:00:22 -0700 Subject: [PATCH] Add support for tab completion (#12411) * Add support for tab completion * make flake8 happy * Add documentation --- docs/_summary.md | 1 + docs/cli_tab_complete.md | 27 ++++++++++ lib/python/qmk/cli/__init__.py | 14 +++++- lib/python/qmk/cli/c2json.py | 7 +-- lib/python/qmk/cli/cformat.py | 3 +- lib/python/qmk/cli/compile.py | 10 ++-- lib/python/qmk/cli/flash.py | 7 +-- lib/python/qmk/cli/generate/config_h.py | 4 +- lib/python/qmk/cli/generate/dfu_header.py | 3 +- lib/python/qmk/cli/generate/info_json.py | 4 +- lib/python/qmk/cli/generate/layouts.py | 4 +- lib/python/qmk/cli/generate/rules_mk.py | 4 +- lib/python/qmk/cli/info.py | 4 +- lib/python/qmk/cli/json2c.py | 3 +- lib/python/qmk/cli/kle2json.py | 3 +- lib/python/qmk/cli/lint.py | 3 +- lib/python/qmk/cli/list/keymaps.py | 4 +- lib/python/qmk/cli/new/keymap.py | 4 +- lib/python/qmk/decorators.py | 60 +++++------------------ lib/python/qmk/keyboard.py | 26 +++++++++- lib/python/qmk/keymap.py | 56 +++++++++++++++++++-- requirements.txt | 2 +- util/qmk_tab_complete.sh | 2 + 23 files changed, 169 insertions(+), 86 deletions(-) create mode 100644 docs/cli_tab_complete.md create mode 100644 util/qmk_tab_complete.sh diff --git a/docs/_summary.md b/docs/_summary.md index 83799acdb8b..825514e6b5d 100644 --- a/docs/_summary.md +++ b/docs/_summary.md @@ -29,6 +29,7 @@ * [Overview](cli.md) * [Configuration](cli_configuration.md) * [Commands](cli_commands.md) + * [Tab Completion](cli_tab_complete.md) * Using QMK * Guides diff --git a/docs/cli_tab_complete.md b/docs/cli_tab_complete.md new file mode 100644 index 00000000000..2217d4fd3bc --- /dev/null +++ b/docs/cli_tab_complete.md @@ -0,0 +1,27 @@ +# Tab Completion for QMK + +If you are using Bash 4.2 or later, Zsh, or FiSH you can enable Tab Completion for the QMK CLI. This will let you tab complete the names of flags, keyboards, files, and other `qmk` options. + +## Setup + +There are several ways you can setup tab completion. + +### For Your User Only + +Add this to the end of your `.profile` or `.bashrc`: + + source ~/qmk_firmware/util/qmk_tab_complete.sh + +If you put `qmk_firmware` into another location you will need to adjust this path. + +### System Wide Symlink + +If you want the tab completion available to all users of the system you can add a symlink to the `qmk_tab_complete.sh` script: + + `ln -s ~/qmk_firmware/util/qmk_tab_complete.sh /etc/profile.d/qmk_tab_complete.sh` + +### System Wide Copy + +In some cases a symlink may not work. Instead you can copy the file directly into place. Be aware that updates to the tab complete script may happen from time to time, you will want to recopy the file periodically. + + cp util/qmk_tab_complete.sh /etc/profile.d diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py index 1349e68a9bf..f7df9081198 100644 --- a/lib/python/qmk/cli/__init__.py +++ b/lib/python/qmk/cli/__init__.py @@ -4,7 +4,7 @@ We list each subcommand here explicitly because all the reliable ways of searchi """ import sys -from milc import cli +from milc import cli, __VERSION__ from . import c2json from . import cformat @@ -47,5 +47,15 @@ from . import pytest # void: 3.9 if sys.version_info[0] != 3 or sys.version_info[1] < 7: - cli.log.error('Your Python is too old! Please upgrade to Python 3.7 or later.') + print('Error: Your Python is too old! Please upgrade to Python 3.7 or later.') + exit(127) + +milc_version = __VERSION__.split('.') + +if int(milc_version[0]) < 2 and int(milc_version[1]) < 3: + from pathlib import Path + + requirements = Path('requirements.txt').resolve() + + print(f'Your MILC library is too old! Please upgrade: python3 -m pip install -U -r {str(requirements)}') exit(127) diff --git a/lib/python/qmk/cli/c2json.py b/lib/python/qmk/cli/c2json.py index 1fa833b647a..e66b0a1b589 100644 --- a/lib/python/qmk/cli/c2json.py +++ b/lib/python/qmk/cli/c2json.py @@ -2,20 +2,21 @@ """ import json +from argcomplete.completers import FilesCompleter from milc import cli import qmk.keymap import qmk.path from qmk.json_encoders import InfoJSONEncoder -from qmk.keyboard import keyboard_folder +from qmk.keyboard import keyboard_completer, keyboard_folder @cli.argument('--no-cpp', arg_only=True, action='store_false', help='Do not use \'cpp\' on keymap.c') @cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") -@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, required=True, help='The keyboard\'s name') +@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, required=True, help='The keyboard\'s name') @cli.argument('-km', '--keymap', arg_only=True, required=True, help='The keymap\'s name') -@cli.argument('filename', arg_only=True, help='keymap.c file') +@cli.argument('filename', arg_only=True, completer=FilesCompleter('.c'), help='keymap.c file') @cli.subcommand('Creates a keymap.json from a keymap.c file.') def c2json(cli): """Generate a keymap.json from a keymap.c file. diff --git a/lib/python/qmk/cli/cformat.py b/lib/python/qmk/cli/cformat.py index c7e93b2ab63..d0d3b3b0a3c 100644 --- a/lib/python/qmk/cli/cformat.py +++ b/lib/python/qmk/cli/cformat.py @@ -3,6 +3,7 @@ import subprocess from shutil import which +from argcomplete.completers import FilesCompleter from milc import cli from qmk.path import normpath @@ -33,7 +34,7 @@ def cformat_run(files, all_files): @cli.argument('-a', '--all-files', arg_only=True, action='store_true', help='Format all core files.') @cli.argument('-b', '--base-branch', default='origin/master', help='Branch to compare to diffs to.') -@cli.argument('files', nargs='*', arg_only=True, help='Filename(s) to format.') +@cli.argument('files', nargs='*', arg_only=True, completer=FilesCompleter('.c'), help='Filename(s) to format.') @cli.subcommand("Format C code according to QMK's style.", hidden=False if cli.config.user.developer else True) def cformat(cli): """Format C code according to QMK's style. diff --git a/lib/python/qmk/cli/compile.py b/lib/python/qmk/cli/compile.py index 5793e989283..23ca4e00a65 100755 --- a/lib/python/qmk/cli/compile.py +++ b/lib/python/qmk/cli/compile.py @@ -2,17 +2,19 @@ You can compile a keymap already in the repo or using a QMK Configurator export. """ +from argcomplete.completers import FilesCompleter from milc import cli import qmk.path from qmk.decorators import automagic_keyboard, automagic_keymap from qmk.commands import compile_configurator_json, create_make_command, parse_configurator_json -from qmk.keyboard import keyboard_folder +from qmk.keyboard import keyboard_completer, keyboard_folder +from qmk.keymap import keymap_completer -@cli.argument('filename', nargs='?', arg_only=True, type=qmk.path.FileType('r'), help='The configurator export to compile') -@cli.argument('-kb', '--keyboard', type=keyboard_folder, 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('filename', nargs='?', arg_only=True, type=qmk.path.FileType('r'), completer=FilesCompleter('.json'), help='The configurator export to compile') +@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='The keyboard to build a firmware for. Ignored when a configurator export is supplied.') +@cli.argument('-km', '--keymap', completer=keymap_completer, 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.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs to run.") @cli.argument('-e', '--env', arg_only=True, action='append', default=[], help="Set a variable to be passed to make. May be passed multiple times.") diff --git a/lib/python/qmk/cli/flash.py b/lib/python/qmk/cli/flash.py index c9273c3f988..1b678406169 100644 --- a/lib/python/qmk/cli/flash.py +++ b/lib/python/qmk/cli/flash.py @@ -4,12 +4,13 @@ You can compile a keymap already in the repo or using a QMK Configurator export. A bootloader must be specified. """ +from argcomplete.completers import FilesCompleter from milc import cli import qmk.path from qmk.decorators import automagic_keyboard, automagic_keymap from qmk.commands import compile_configurator_json, create_make_command, parse_configurator_json -from qmk.keyboard import keyboard_folder +from qmk.keyboard import keyboard_completer, keyboard_folder def print_bootloader_help(): @@ -30,11 +31,11 @@ def print_bootloader_help(): cli.echo('For more info, visit https://docs.qmk.fm/#/flashing') -@cli.argument('filename', nargs='?', arg_only=True, type=qmk.path.FileType('r'), help='The configurator export JSON to compile.') +@cli.argument('filename', nargs='?', arg_only=True, type=qmk.path.FileType('r'), completer=FilesCompleter('.json'), 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', type=keyboard_folder, 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('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, 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('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually build, just show the make command to be run.") @cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs to run.") @cli.argument('-e', '--env', arg_only=True, action='append', default=[], help="Set a variable to be passed to make. May be passed multiple times.") diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py index ccea6d7a05d..54cd5b96a87 100755 --- a/lib/python/qmk/cli/generate/config_h.py +++ b/lib/python/qmk/cli/generate/config_h.py @@ -8,7 +8,7 @@ from milc import cli from qmk.decorators import automagic_keyboard, automagic_keymap from qmk.info import info_json from qmk.json_schema import json_load -from qmk.keyboard import keyboard_folder +from qmk.keyboard import keyboard_completer, keyboard_folder from qmk.path import is_keyboard, normpath @@ -75,7 +75,7 @@ def matrix_pins(matrix_pins): @cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to') @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") -@cli.argument('-kb', '--keyboard', type=keyboard_folder, help='Keyboard to generate config.h for.') +@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate config.h for.') @cli.subcommand('Used by the make system to generate info_config.h from info.json', hidden=True) @automagic_keyboard @automagic_keymap diff --git a/lib/python/qmk/cli/generate/dfu_header.py b/lib/python/qmk/cli/generate/dfu_header.py index 6f958b3a3df..211ed9991a1 100644 --- a/lib/python/qmk/cli/generate/dfu_header.py +++ b/lib/python/qmk/cli/generate/dfu_header.py @@ -6,11 +6,12 @@ from milc import cli from qmk.decorators import automagic_keyboard from qmk.info import info_json from qmk.path import is_keyboard, normpath +from qmk.keyboard import keyboard_completer @cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to') @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") -@cli.argument('-kb', '--keyboard', help='Keyboard to generate LUFA Keyboard.h for.') +@cli.argument('-kb', '--keyboard', completer=keyboard_completer, help='Keyboard to generate LUFA Keyboard.h for.') @cli.subcommand('Used by the make system to generate LUFA Keyboard.h from info.json', hidden=True) @automagic_keyboard def generate_dfu_header(cli): diff --git a/lib/python/qmk/cli/generate/info_json.py b/lib/python/qmk/cli/generate/info_json.py index 1af7f04392e..8931b68b6ff 100755 --- a/lib/python/qmk/cli/generate/info_json.py +++ b/lib/python/qmk/cli/generate/info_json.py @@ -11,7 +11,7 @@ from qmk.decorators import automagic_keyboard, automagic_keymap from qmk.info import info_json from qmk.json_encoders import InfoJSONEncoder from qmk.json_schema import load_jsonschema -from qmk.keyboard import keyboard_folder +from qmk.keyboard import keyboard_completer, keyboard_folder from qmk.path import is_keyboard @@ -41,7 +41,7 @@ def strip_info_json(kb_info_json): return validator(kb_info_json) -@cli.argument('-kb', '--keyboard', type=keyboard_folder, help='Keyboard to show info for.') +@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Keyboard to show info for.') @cli.argument('-km', '--keymap', help='Show the layers for a JSON keymap too.') @cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True) @automagic_keyboard diff --git a/lib/python/qmk/cli/generate/layouts.py b/lib/python/qmk/cli/generate/layouts.py index 7b4394291f6..ad6946d6cf1 100755 --- a/lib/python/qmk/cli/generate/layouts.py +++ b/lib/python/qmk/cli/generate/layouts.py @@ -5,7 +5,7 @@ from milc import cli from qmk.constants import COL_LETTERS, ROW_LETTERS from qmk.decorators import automagic_keyboard, automagic_keymap from qmk.info import info_json -from qmk.keyboard import keyboard_folder +from qmk.keyboard import keyboard_completer, keyboard_folder from qmk.path import is_keyboard, normpath usb_properties = { @@ -17,7 +17,7 @@ usb_properties = { @cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to') @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") -@cli.argument('-kb', '--keyboard', type=keyboard_folder, help='Keyboard to generate config.h for.') +@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate config.h for.') @cli.subcommand('Used by the make system to generate layouts.h from info.json', hidden=True) @automagic_keyboard @automagic_keymap diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py index 91759d26c64..41c94e16b55 100755 --- a/lib/python/qmk/cli/generate/rules_mk.py +++ b/lib/python/qmk/cli/generate/rules_mk.py @@ -8,7 +8,7 @@ from milc import cli from qmk.decorators import automagic_keyboard, automagic_keymap from qmk.info import info_json from qmk.json_schema import json_load -from qmk.keyboard import keyboard_folder +from qmk.keyboard import keyboard_completer, keyboard_folder from qmk.path import is_keyboard, normpath @@ -39,7 +39,7 @@ def process_mapping_rule(kb_info_json, rules_key, info_dict): @cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to') @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") @cli.argument('-e', '--escape', arg_only=True, action='store_true', help="Escape spaces in quiet mode") -@cli.argument('-kb', '--keyboard', type=keyboard_folder, help='Keyboard to generate config.h for.') +@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate config.h for.') @cli.subcommand('Used by the make system to generate info_config.h from info.json', hidden=True) @automagic_keyboard @automagic_keymap diff --git a/lib/python/qmk/cli/info.py b/lib/python/qmk/cli/info.py index aac507c1a5d..572b305cacb 100755 --- a/lib/python/qmk/cli/info.py +++ b/lib/python/qmk/cli/info.py @@ -10,7 +10,7 @@ from milc import cli from qmk.json_encoders import InfoJSONEncoder from qmk.constants import COL_LETTERS, ROW_LETTERS from qmk.decorators import automagic_keyboard, automagic_keymap -from qmk.keyboard import keyboard_folder, render_layouts, render_layout +from qmk.keyboard import keyboard_completer, keyboard_folder, render_layouts, render_layout from qmk.keymap import locate_keymap from qmk.info import info_json from qmk.path import is_keyboard @@ -124,7 +124,7 @@ def print_text_output(kb_info_json): show_keymap(kb_info_json, False) -@cli.argument('-kb', '--keyboard', type=keyboard_folder, help='Keyboard to show info for.') +@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Keyboard to show info for.') @cli.argument('-km', '--keymap', help='Show the layers for a JSON keymap too.') @cli.argument('-l', '--layouts', action='store_true', help='Render the layouts.') @cli.argument('-m', '--matrix', action='store_true', help='Render the layouts with matrix information.') diff --git a/lib/python/qmk/cli/json2c.py b/lib/python/qmk/cli/json2c.py index 5a2fb96c788..a90578c021d 100755 --- a/lib/python/qmk/cli/json2c.py +++ b/lib/python/qmk/cli/json2c.py @@ -2,6 +2,7 @@ """ import json +from argcomplete.completers import FilesCompleter from milc import cli import qmk.keymap @@ -10,7 +11,7 @@ import qmk.path @cli.argument('-o', '--output', arg_only=True, type=qmk.path.normpath, help='File to write to') @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") -@cli.argument('filename', type=qmk.path.FileType('r'), arg_only=True, help='Configurator JSON file') +@cli.argument('filename', type=qmk.path.FileType('r'), arg_only=True, completer=FilesCompleter('.json'), help='Configurator JSON file') @cli.subcommand('Creates a keymap.c from a QMK Configurator export.') def json2c(cli): """Generate a keymap.c from a configurator export. diff --git a/lib/python/qmk/cli/kle2json.py b/lib/python/qmk/cli/kle2json.py index 91499c9af32..acb75ef4fdb 100755 --- a/lib/python/qmk/cli/kle2json.py +++ b/lib/python/qmk/cli/kle2json.py @@ -4,6 +4,7 @@ import json import os from pathlib import Path +from argcomplete.completers import FilesCompleter from milc import cli from kle2xy import KLE2xy @@ -11,7 +12,7 @@ from qmk.converter import kle2qmk from qmk.json_encoders import InfoJSONEncoder -@cli.argument('filename', help='The KLE raw txt to convert') +@cli.argument('filename', completer=FilesCompleter('.json'), help='The KLE raw txt to convert') @cli.argument('-f', '--force', action='store_true', help='Flag to overwrite current info.json') @cli.subcommand('Convert a KLE layout to a Configurator JSON', hidden=False if cli.config.user.developer else True) def kle2json(cli): diff --git a/lib/python/qmk/cli/lint.py b/lib/python/qmk/cli/lint.py index 74467021e07..a164dba6324 100644 --- a/lib/python/qmk/cli/lint.py +++ b/lib/python/qmk/cli/lint.py @@ -4,12 +4,13 @@ from milc import cli from qmk.decorators import automagic_keyboard, automagic_keymap from qmk.info import info_json +from qmk.keyboard import keyboard_completer from qmk.keymap import locate_keymap from qmk.path import is_keyboard, keyboard @cli.argument('--strict', action='store_true', help='Treat warnings as errors.') -@cli.argument('-kb', '--keyboard', help='The keyboard to check.') +@cli.argument('-kb', '--keyboard', completer=keyboard_completer, help='The keyboard to check.') @cli.argument('-km', '--keymap', help='The keymap to check.') @cli.subcommand('Check keyboard and keymap for common mistakes.') @automagic_keyboard diff --git a/lib/python/qmk/cli/list/keymaps.py b/lib/python/qmk/cli/list/keymaps.py index 7c0ad43997d..d79ab75b582 100644 --- a/lib/python/qmk/cli/list/keymaps.py +++ b/lib/python/qmk/cli/list/keymaps.py @@ -4,10 +4,10 @@ from milc import cli import qmk.keymap from qmk.decorators import automagic_keyboard -from qmk.keyboard import keyboard_folder +from qmk.keyboard import keyboard_completer, keyboard_folder -@cli.argument("-kb", "--keyboard", type=keyboard_folder, help="Specify keyboard name. Example: 1upkeyboards/1up60hse") +@cli.argument("-kb", "--keyboard", type=keyboard_folder, completer=keyboard_completer, help="Specify keyboard name. Example: 1upkeyboards/1up60hse") @cli.subcommand("List the keymaps for a specific keyboard") @automagic_keyboard def list_keymaps(cli): diff --git a/lib/python/qmk/cli/new/keymap.py b/lib/python/qmk/cli/new/keymap.py index ea98a287c16..60cb743cb66 100755 --- a/lib/python/qmk/cli/new/keymap.py +++ b/lib/python/qmk/cli/new/keymap.py @@ -5,11 +5,11 @@ from pathlib import Path import qmk.path from qmk.decorators import automagic_keyboard, automagic_keymap -from qmk.keyboard import keyboard_folder +from qmk.keyboard import keyboard_completer, keyboard_folder from milc import cli -@cli.argument('-kb', '--keyboard', type=keyboard_folder, help='Specify keyboard name. Example: 1upkeyboards/1up60hse') +@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, 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 diff --git a/lib/python/qmk/decorators.py b/lib/python/qmk/decorators.py index 629402b0957..8d43ae980f5 100644 --- a/lib/python/qmk/decorators.py +++ b/lib/python/qmk/decorators.py @@ -1,13 +1,12 @@ """Helpful decorators that subcommands can use. """ import functools -from pathlib import Path from time import monotonic from milc import cli -from qmk.keymap import is_keymap_dir -from qmk.path import is_keyboard, under_qmk_firmware +from qmk.keyboard import find_keyboard_from_dir +from qmk.keymap import find_keymap_from_dir def automagic_keyboard(func): @@ -17,27 +16,13 @@ def automagic_keyboard(func): """ @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] + keyboard = find_keyboard_from_dir() - if is_keyboard(current_path): - cli.config[cli._entrypoint.__name__]['keyboard'] = str(current_path) - cli.config_source[cli._entrypoint.__name__]['keyboard'] = 'keyboard_directory' + if keyboard: + cli.config[cli._entrypoint.__name__]['keyboard'] = keyboard + cli.config_source[cli._entrypoint.__name__]['keyboard'] = 'keyboard_directory' return func(*args, **kwargs) @@ -51,36 +36,13 @@ def automagic_keymap(func): """ @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__]['keymap'] = '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__]['keymap'] = '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__]['keymap'] = 'users_directory' + keymap_name, keymap_type = find_keymap_from_dir() + + if keymap_name: + cli.config[cli._entrypoint.__name__]['keymap'] = keymap_name + cli.config_source[cli._entrypoint.__name__]['keymap'] = keymap_type return func(*args, **kwargs) diff --git a/lib/python/qmk/keyboard.py b/lib/python/qmk/keyboard.py index 89f9346c406..0168d17ef39 100644 --- a/lib/python/qmk/keyboard.py +++ b/lib/python/qmk/keyboard.py @@ -9,7 +9,7 @@ from glob import glob from qmk.c_parse import parse_config_h_file from qmk.json_schema import json_load from qmk.makefile import parse_rules_mk_file -from qmk.path import is_keyboard +from qmk.path import is_keyboard, under_qmk_firmware BOX_DRAWING_CHARACTERS = { "unicode": { @@ -33,6 +33,24 @@ BOX_DRAWING_CHARACTERS = { base_path = os.path.join(os.getcwd(), "keyboards") + os.path.sep +def find_keyboard_from_dir(): + """Returns a keyboard name based on the user's current directory. + """ + 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): + return str(current_path) + + def keyboard_folder(keyboard): """Returns the actual keyboard folder. @@ -61,6 +79,12 @@ def _find_name(path): return path.replace(base_path, "").replace(os.path.sep + "rules.mk", "") +def keyboard_completer(prefix, action, parser, parsed_args): + """Returns a list of keyboards for tab completion. + """ + return list_keyboards() + + def list_keyboards(): """Returns a list of all keyboards. """ diff --git a/lib/python/qmk/keymap.py b/lib/python/qmk/keymap.py index d8495c38bcf..4ad9ffb5913 100644 --- a/lib/python/qmk/keymap.py +++ b/lib/python/qmk/keymap.py @@ -1,19 +1,19 @@ """Functions that help you work with QMK keymaps. """ -from pathlib import Path import json import subprocess import sys +from pathlib import Path +import argcomplete +from milc import cli from pygments.lexers.c_cpp import CLexer from pygments.token import Token from pygments import lex -from milc import cli - -from qmk.keyboard import rules_mk import qmk.path import qmk.commands +from qmk.keyboard import find_keyboard_from_dir, rules_mk # The `keymap.c` template to use when a keyboard doesn't have its own DEFAULT_KEYMAP_C = """#include QMK_KEYBOARD_H @@ -74,6 +74,54 @@ def _strip_any(keycode): return keycode +def find_keymap_from_dir(): + """Returns `(keymap_name, source)` for the directory we're currently in. + + """ + relative_cwd = qmk.path.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 + + return current_path.name, '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): + return relative_cwd.name, '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 + return relative_cwd.parts[1], 'users_directory' + + return None, None + + +def keymap_completer(prefix, action, parser, parsed_args): + """Returns a list of keymaps for tab completion. + """ + try: + if parsed_args.keyboard: + return list_keymaps(parsed_args.keyboard) + + keyboard = find_keyboard_from_dir() + + if keyboard: + return list_keymaps(keyboard) + + except Exception as e: + argcomplete.warn(f'Error: {e.__class__.__name__}: {str(e)}') + return [] + + return [] + + def is_keymap_dir(keymap, c=True, json=True, additional_files=None): """Return True if Path object `keymap` has a keymap file inside. diff --git a/requirements.txt b/requirements.txt index f74cb73cb72..8553e2c3f02 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,5 +5,5 @@ colorama dotty-dict hjson jsonschema>=3 -milc>=1.1.0 +milc>=1.3.0 pygments diff --git a/util/qmk_tab_complete.sh b/util/qmk_tab_complete.sh new file mode 100644 index 00000000000..ebcb5536acf --- /dev/null +++ b/util/qmk_tab_complete.sh @@ -0,0 +1,2 @@ +# Register qmk with tab completion +eval "$(register-python-argcomplete --no-defaults qmk)"