diff --git a/README.md b/README.md index c283c75..1a2e985 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,21 @@ Finally they are exiting the current URI so they can enable another URI. The use doesn't need to worry about which version of the Maya or Houdini applications they should launch, that is configured for them by the URI they pass to `hab env`. +#### Tab Completion + +You can enable tab completion including known URI's and aliases when using bash. +This requires using bash 4.4 or newer. To do this you need to source the +`.hab-complete.bash` file. This is installed next to the `hab` script (This makes +its path consistent even with editable installs). Use `which .hab-complete.bash` +to locate the file. If that doesn't work it should be next to the file returned +by `which hab`. + +Examples of what to add to .bashrc +- Windows: `. /c/Program\ Files/Python39/Scripts/.hab-complete.bash` +- Linux: `. /usr/local/bin/.hab-complete.bash` + +See https://click.palletsprojects.com/en/8.1.x/shell-completion/ for more details. + ### Looking up aliases In the previous section the use knew that they could run maya and houdini. You can diff --git a/bin/.hab-complete.bash b/bin/.hab-complete.bash new file mode 100644 index 0000000..7f3d38c --- /dev/null +++ b/bin/.hab-complete.bash @@ -0,0 +1,37 @@ +# This script enables tab completion for hab in bash. It requires bash 4.4+. +# https://click.palletsprojects.com/en/8.1.x/shell-completion/ + +# To enable tab completion source this script in your .bashrc script. +# `. /c/Program\ Files/Python39/Scripts/.hab-complete.bash` +# `. /usr/local/bin/.hab-complete.bash` + +# This script below was generated by running `_HAB_COMPLETE=bash_source hab > .hab-complete.bash` + +_hab_completion() { + local IFS=$'\n' + local response + + response=$(env COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD _HAB_COMPLETE=bash_complete $1) + + for completion in $response; do + IFS=',' read type value <<< "$completion" + + if [[ $type == 'dir' ]]; then + COMPREPLY=() + compopt -o dirnames + elif [[ $type == 'file' ]]; then + COMPREPLY=() + compopt -o default + elif [[ $type == 'plain' ]]; then + COMPREPLY+=($value) + fi + done + + return 0 +} + +_hab_completion_setup() { + complete -o nosort -F _hab_completion hab +} + +_hab_completion_setup; diff --git a/hab/__main__.py b/hab/__main__.py index 590d5b4..eca344c 100644 --- a/hab/__main__.py +++ b/hab/__main__.py @@ -10,4 +10,4 @@ # prog_name prevents __main__.py from being shown as the command name in the help # text. We don't know the exact command the user passed so we provide a generic # `python -m hab` command. - sys.exit(hab.cli.cli(prog_name="python -m hab")) + sys.exit(hab.cli.cli(prog_name="hab")) diff --git a/hab/cli.py b/hab/cli.py index 4dcbd0a..3e4c212 100644 --- a/hab/cli.py +++ b/hab/cli.py @@ -6,6 +6,7 @@ from pathlib import Path import click +from click.shell_completion import CompletionItem from colorama import Fore from . import Resolver, Site, __version__, utils @@ -18,6 +19,21 @@ CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) +def complete_alias(ctx, param, incomplete): + """Dynamic tab completion for shell_complete generating available aliases""" + resolver = ctx.obj.resolver + if not ctx.obj.uri: + # The URI hasn't been resolved, so don't have any aliases to return. + return [] + # Resolve the config from the URI stored in `UriArgument.type_cast_value`. + cfg = resolver.resolve(ctx.obj.uri) + return [ + CompletionItem(alias) + for alias in cfg.aliases + if alias.strip().startswith(incomplete) + ] + + class UriArgument(click.Argument): """Accepts a URI string, frozen string or path to frozen json file. @@ -54,6 +70,8 @@ class UriArgument(click.Argument): def __init__(self, *args, prompt=True, **kwargs): self.prompt = prompt + # Override shell_complete with our implementation by default + kwargs.setdefault("shell_complete", self.complete_uri) super().__init__(*args, **kwargs) def __uri_prompt(self, uri=None): @@ -75,6 +93,15 @@ def __uri_prompt(self, uri=None): ) return response + def complete_uri(self, ctx, param, incomplete): + """Dynamic tab completion for shell_complete generating available URI's""" + resolver = ctx.obj.resolver + return [ + CompletionItem(uri.strip()) + for uri in resolver.dump_forest(resolver.configs) + if uri.strip().startswith(incomplete) + ] + def type_cast_value(self, ctx, value): """Convert and validate the uri value. This override handles saving the uri to user prefs if enabled by the cli. @@ -150,6 +177,9 @@ def type_cast_value(self, ctx, value): if ctx.obj.enable_user_prefs_save: ctx.obj.resolver.user_prefs().uri = value + # Store the URI so it can be used by other shell_complete functions. + ctx.obj.uri = value + return value @@ -541,6 +571,9 @@ def set_uri(settings, uri): "--launch", default=None, help="Run this alias after activating. This leaves the new shell active.", + # TODO: This requires resolving the uri argument before this option. Click + # doesn't support that by default. Figure out how to get this working. + # shell_complete=complete_alias, ) @click.pass_obj def env(settings, uri, launch): @@ -682,6 +715,9 @@ def echo_line(line): "--launch", default=None, help="Run this alias after activating. This leaves the new shell activated.", + # TODO: This requires resolving the uri argument before this option. Click + # doesn't support that by default. Figure out how to get this working. + # shell_complete=complete_alias, ) @click.pass_obj def activate(settings, uri, launch): @@ -717,7 +753,7 @@ def activate(settings, uri, launch): cls=UriHelpClass, ) @click.argument("uri", cls=UriArgument) -@click.argument("alias") +@click.argument("alias", shell_complete=complete_alias) # Pass all remaining arguments to the requested alias @click.argument("args", nargs=-1, type=click.UNPROCESSED) @click.pass_obj diff --git a/setup.cfg b/setup.cfg index 02ebf92..68ca54a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -41,6 +41,7 @@ install_requires = python_requires = >=3.6 include_package_data = True scripts = + bin/.hab-complete.bash bin/hab.bat bin/hab.ps1 bin/hab