Browse Source

Add support for tab completion (#12411)

* Add support for tab completion

* make flake8 happy

* Add documentation
Zach White 4 years ago
parent
commit
588bcdc8ca

+ 1 - 0
docs/_summary.md

@@ -29,6 +29,7 @@
     * [Overview](cli.md)
     * [Overview](cli.md)
     * [Configuration](cli_configuration.md)
     * [Configuration](cli_configuration.md)
     * [Commands](cli_commands.md)
     * [Commands](cli_commands.md)
+    * [Tab Completion](cli_tab_complete.md)
 
 
 * Using QMK
 * Using QMK
   * Guides
   * Guides

+ 27 - 0
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

+ 12 - 2
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
 import sys
 
 
-from milc import cli
+from milc import cli, __VERSION__
 
 
 from . import c2json
 from . import c2json
 from . import cformat
 from . import cformat
@@ -47,5 +47,15 @@ from . import pytest
 # void: 3.9
 # void: 3.9
 
 
 if sys.version_info[0] != 3 or sys.version_info[1] < 7:
 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)
     exit(127)

+ 4 - 3
lib/python/qmk/cli/c2json.py

@@ -2,20 +2,21 @@
 """
 """
 import json
 import json
 
 
+from argcomplete.completers import FilesCompleter
 from milc import cli
 from milc import cli
 
 
 import qmk.keymap
 import qmk.keymap
 import qmk.path
 import qmk.path
 from qmk.json_encoders import InfoJSONEncoder
 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('--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('-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('-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('-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.')
 @cli.subcommand('Creates a keymap.json from a keymap.c file.')
 def c2json(cli):
 def c2json(cli):
     """Generate a keymap.json from a keymap.c file.
     """Generate a keymap.json from a keymap.c file.

+ 2 - 1
lib/python/qmk/cli/cformat.py

@@ -3,6 +3,7 @@
 import subprocess
 import subprocess
 from shutil import which
 from shutil import which
 
 
+from argcomplete.completers import FilesCompleter
 from milc import cli
 from milc import cli
 
 
 from qmk.path import normpath
 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('-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('-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)
 @cli.subcommand("Format C code according to QMK's style.", hidden=False if cli.config.user.developer else True)
 def cformat(cli):
 def cformat(cli):
     """Format C code according to QMK's style.
     """Format C code according to QMK's style.

+ 6 - 4
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.
 You can compile a keymap already in the repo or using a QMK Configurator export.
 """
 """
+from argcomplete.completers import FilesCompleter
 from milc import cli
 from milc import cli
 
 
 import qmk.path
 import qmk.path
 from qmk.decorators import automagic_keyboard, automagic_keymap
 from qmk.decorators import automagic_keyboard, automagic_keymap
 from qmk.commands import compile_configurator_json, create_make_command, parse_configurator_json
 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('-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('-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.")
 @cli.argument('-e', '--env', arg_only=True, action='append', default=[], help="Set a variable to be passed to make. May be passed multiple times.")

+ 4 - 3
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.
 A bootloader must be specified.
 """
 """
 
 
+from argcomplete.completers import FilesCompleter
 from milc import cli
 from milc import cli
 
 
 import qmk.path
 import qmk.path
 from qmk.decorators import automagic_keyboard, automagic_keymap
 from qmk.decorators import automagic_keyboard, automagic_keymap
 from qmk.commands import compile_configurator_json, create_make_command, parse_configurator_json
 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():
 def print_bootloader_help():
@@ -30,11 +31,11 @@ def print_bootloader_help():
     cli.echo('For more info, visit https://docs.qmk.fm/#/flashing')
     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('-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('-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('-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('-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('-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.")
 @cli.argument('-e', '--env', arg_only=True, action='append', default=[], help="Set a variable to be passed to make. May be passed multiple times.")

+ 2 - 2
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.decorators import automagic_keyboard, automagic_keymap
 from qmk.info import info_json
 from qmk.info import info_json
 from qmk.json_schema import json_load
 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
 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('-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('-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)
 @cli.subcommand('Used by the make system to generate info_config.h from info.json', hidden=True)
 @automagic_keyboard
 @automagic_keyboard
 @automagic_keymap
 @automagic_keymap

+ 2 - 1
lib/python/qmk/cli/generate/dfu_header.py

@@ -6,11 +6,12 @@ from milc import cli
 from qmk.decorators import automagic_keyboard
 from qmk.decorators import automagic_keyboard
 from qmk.info import info_json
 from qmk.info import info_json
 from qmk.path import is_keyboard, normpath
 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('-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('-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)
 @cli.subcommand('Used by the make system to generate LUFA Keyboard.h from info.json', hidden=True)
 @automagic_keyboard
 @automagic_keyboard
 def generate_dfu_header(cli):
 def generate_dfu_header(cli):

+ 2 - 2
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.info import info_json
 from qmk.json_encoders import InfoJSONEncoder
 from qmk.json_encoders import InfoJSONEncoder
 from qmk.json_schema import load_jsonschema
 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
 from qmk.path import is_keyboard
 
 
 
 
@@ -41,7 +41,7 @@ def strip_info_json(kb_info_json):
     return validator(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.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)
 @cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True)
 @automagic_keyboard
 @automagic_keyboard

+ 2 - 2
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.constants import COL_LETTERS, ROW_LETTERS
 from qmk.decorators import automagic_keyboard, automagic_keymap
 from qmk.decorators import automagic_keyboard, automagic_keymap
 from qmk.info import info_json
 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
 from qmk.path import is_keyboard, normpath
 
 
 usb_properties = {
 usb_properties = {
@@ -17,7 +17,7 @@ usb_properties = {
 
 
 @cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
 @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('-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)
 @cli.subcommand('Used by the make system to generate layouts.h from info.json', hidden=True)
 @automagic_keyboard
 @automagic_keyboard
 @automagic_keymap
 @automagic_keymap

+ 2 - 2
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.decorators import automagic_keyboard, automagic_keymap
 from qmk.info import info_json
 from qmk.info import info_json
 from qmk.json_schema import json_load
 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
 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('-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('-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('-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)
 @cli.subcommand('Used by the make system to generate info_config.h from info.json', hidden=True)
 @automagic_keyboard
 @automagic_keyboard
 @automagic_keymap
 @automagic_keymap

+ 2 - 2
lib/python/qmk/cli/info.py

@@ -10,7 +10,7 @@ from milc import cli
 from qmk.json_encoders import InfoJSONEncoder
 from qmk.json_encoders import InfoJSONEncoder
 from qmk.constants import COL_LETTERS, ROW_LETTERS
 from qmk.constants import COL_LETTERS, ROW_LETTERS
 from qmk.decorators import automagic_keyboard, automagic_keymap
 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.keymap import locate_keymap
 from qmk.info import info_json
 from qmk.info import info_json
 from qmk.path import is_keyboard
 from qmk.path import is_keyboard
@@ -124,7 +124,7 @@ def print_text_output(kb_info_json):
         show_keymap(kb_info_json, False)
         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('-km', '--keymap', help='Show the layers for a JSON keymap too.')
 @cli.argument('-l', '--layouts', action='store_true', help='Render the layouts.')
 @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.')
 @cli.argument('-m', '--matrix', action='store_true', help='Render the layouts with matrix information.')

+ 2 - 1
lib/python/qmk/cli/json2c.py

@@ -2,6 +2,7 @@
 """
 """
 import json
 import json
 
 
+from argcomplete.completers import FilesCompleter
 from milc import cli
 from milc import cli
 
 
 import qmk.keymap
 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('-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('-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.')
 @cli.subcommand('Creates a keymap.c from a QMK Configurator export.')
 def json2c(cli):
 def json2c(cli):
     """Generate a keymap.c from a configurator export.
     """Generate a keymap.c from a configurator export.

+ 2 - 1
lib/python/qmk/cli/kle2json.py

@@ -4,6 +4,7 @@ import json
 import os
 import os
 from pathlib import Path
 from pathlib import Path
 
 
+from argcomplete.completers import FilesCompleter
 from milc import cli
 from milc import cli
 from kle2xy import KLE2xy
 from kle2xy import KLE2xy
 
 
@@ -11,7 +12,7 @@ from qmk.converter import kle2qmk
 from qmk.json_encoders import InfoJSONEncoder
 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.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)
 @cli.subcommand('Convert a KLE layout to a Configurator JSON', hidden=False if cli.config.user.developer else True)
 def kle2json(cli):
 def kle2json(cli):

+ 2 - 1
lib/python/qmk/cli/lint.py

@@ -4,12 +4,13 @@ from milc import cli
 
 
 from qmk.decorators import automagic_keyboard, automagic_keymap
 from qmk.decorators import automagic_keyboard, automagic_keymap
 from qmk.info import info_json
 from qmk.info import info_json
+from qmk.keyboard import keyboard_completer
 from qmk.keymap import locate_keymap
 from qmk.keymap import locate_keymap
 from qmk.path import is_keyboard, keyboard
 from qmk.path import is_keyboard, keyboard
 
 
 
 
 @cli.argument('--strict', action='store_true', help='Treat warnings as errors.')
 @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.argument('-km', '--keymap', help='The keymap to check.')
 @cli.subcommand('Check keyboard and keymap for common mistakes.')
 @cli.subcommand('Check keyboard and keymap for common mistakes.')
 @automagic_keyboard
 @automagic_keyboard

+ 2 - 2
lib/python/qmk/cli/list/keymaps.py

@@ -4,10 +4,10 @@ from milc import cli
 
 
 import qmk.keymap
 import qmk.keymap
 from qmk.decorators import automagic_keyboard
 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")
 @cli.subcommand("List the keymaps for a specific keyboard")
 @automagic_keyboard
 @automagic_keyboard
 def list_keymaps(cli):
 def list_keymaps(cli):

+ 2 - 2
lib/python/qmk/cli/new/keymap.py

@@ -5,11 +5,11 @@ from pathlib import Path
 
 
 import qmk.path
 import qmk.path
 from qmk.decorators import automagic_keyboard, automagic_keymap
 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
 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.argument('-km', '--keymap', help='Specify the name for the new keymap directory')
 @cli.subcommand('Creates a new keymap for the keyboard of your choosing')
 @cli.subcommand('Creates a new keymap for the keyboard of your choosing')
 @automagic_keyboard
 @automagic_keyboard

+ 11 - 49
lib/python/qmk/decorators.py

@@ -1,13 +1,12 @@
 """Helpful decorators that subcommands can use.
 """Helpful decorators that subcommands can use.
 """
 """
 import functools
 import functools
-from pathlib import Path
 from time import monotonic
 from time import monotonic
 
 
 from milc import cli
 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):
 def automagic_keyboard(func):
@@ -17,27 +16,13 @@ def automagic_keyboard(func):
     """
     """
     @functools.wraps(func)
     @functools.wraps(func)
     def wrapper(*args, **kwargs):
     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`
         # Ensure that `--keyboard` was not passed and CWD is under `qmk_firmware/keyboards`
         if cli.config_source[cli._entrypoint.__name__]['keyboard'] != 'argument':
         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)
         return func(*args, **kwargs)
 
 
@@ -51,36 +36,13 @@ def automagic_keymap(func):
     """
     """
     @functools.wraps(func)
     @functools.wraps(func)
     def wrapper(*args, **kwargs):
     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`
         # Ensure that `--keymap` was not passed and that we're under `qmk_firmware`
         if cli.config_source[cli._entrypoint.__name__]['keymap'] != 'argument':
         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)
         return func(*args, **kwargs)
 
 

+ 25 - 1
lib/python/qmk/keyboard.py

@@ -9,7 +9,7 @@ from glob import glob
 from qmk.c_parse import parse_config_h_file
 from qmk.c_parse import parse_config_h_file
 from qmk.json_schema import json_load
 from qmk.json_schema import json_load
 from qmk.makefile import parse_rules_mk_file
 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 = {
 BOX_DRAWING_CHARACTERS = {
     "unicode": {
     "unicode": {
@@ -33,6 +33,24 @@ BOX_DRAWING_CHARACTERS = {
 base_path = os.path.join(os.getcwd(), "keyboards") + os.path.sep
 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):
 def keyboard_folder(keyboard):
     """Returns the actual keyboard folder.
     """Returns the actual keyboard folder.
 
 
@@ -61,6 +79,12 @@ def _find_name(path):
     return path.replace(base_path, "").replace(os.path.sep + "rules.mk", "")
     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():
 def list_keyboards():
     """Returns a list of all keyboards.
     """Returns a list of all keyboards.
     """
     """

+ 52 - 4
lib/python/qmk/keymap.py

@@ -1,19 +1,19 @@
 """Functions that help you work with QMK keymaps.
 """Functions that help you work with QMK keymaps.
 """
 """
-from pathlib import Path
 import json
 import json
 import subprocess
 import subprocess
 import sys
 import sys
+from pathlib import Path
 
 
+import argcomplete
+from milc import cli
 from pygments.lexers.c_cpp import CLexer
 from pygments.lexers.c_cpp import CLexer
 from pygments.token import Token
 from pygments.token import Token
 from pygments import lex
 from pygments import lex
 
 
-from milc import cli
-
-from qmk.keyboard import rules_mk
 import qmk.path
 import qmk.path
 import qmk.commands
 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
 # The `keymap.c` template to use when a keyboard doesn't have its own
 DEFAULT_KEYMAP_C = """#include QMK_KEYBOARD_H
 DEFAULT_KEYMAP_C = """#include QMK_KEYBOARD_H
@@ -74,6 +74,54 @@ def _strip_any(keycode):
     return 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):
 def is_keymap_dir(keymap, c=True, json=True, additional_files=None):
     """Return True if Path object `keymap` has a keymap file inside.
     """Return True if Path object `keymap` has a keymap file inside.
 
 

+ 1 - 1
requirements.txt

@@ -5,5 +5,5 @@ colorama
 dotty-dict
 dotty-dict
 hjson
 hjson
 jsonschema>=3
 jsonschema>=3
-milc>=1.1.0
+milc>=1.3.0
 pygments
 pygments

+ 2 - 0
util/qmk_tab_complete.sh

@@ -0,0 +1,2 @@
+# Register qmk with tab completion
+eval "$(register-python-argcomplete --no-defaults qmk)"