diff --git a/HISTORY.rst b/HISTORY.rst index fa9aa35d..4a4bd78f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -12,6 +12,7 @@ PlatformIO Core 5 ~~~~~~~~~~~~~~~~~~ - Added support for a new ``headers`` field in `library.json `__ (declare a list of header files that can be included in a project source files using ``#include <...>`` directive) +- Improved tab completion support for Bash, ZSH, and Fish shells (`issue #4114 `_) - Improved support for projects located on a network share (`issue #3417 `_, `issue #3926 `_, `issue #4099 `_) - Upgraded build engine to the SCons 4.3 (`release notes `__) - Fixed an issue with the CLion project generator when a macro contains a space (`issue #4102 `_) diff --git a/docs b/docs index ad3f9b62..b6322c06 160000 --- a/docs +++ b/docs @@ -1 +1 @@ -Subproject commit ad3f9b62cf80b679b6d11162a725f2bc3462297d +Subproject commit b6322c062fd6ea58d8e6bca737a51833635d2173 diff --git a/platformio/__main__.py b/platformio/__main__.py index f7bdd61b..d3b765fb 100644 --- a/platformio/__main__.py +++ b/platformio/__main__.py @@ -24,13 +24,6 @@ from platformio import __version__, exception from platformio.commands import PlatformioCLI from platformio.compat import IS_CYGWIN, ensure_python3 -try: - import click_completion # pylint: disable=import-error - - click_completion.init() -except: # pylint: disable=bare-except - pass - @click.command( cls=PlatformioCLI, context_settings=dict(help_option_names=["-h", "--help"]) diff --git a/platformio/commands/system/command.py b/platformio/commands/system/command.py index c207d0ae..a75007d6 100644 --- a/platformio/commands/system/command.py +++ b/platformio/commands/system/command.py @@ -14,7 +14,6 @@ import json import platform -import subprocess import sys import click @@ -22,6 +21,7 @@ from tabulate import tabulate from platformio import __version__, compat, fs, proc, util from platformio.commands.system.completion import ( + ShellType, get_completion_install_path, install_completion_code, uninstall_completion_code, @@ -150,23 +150,11 @@ def system_prune(force, dry_run, cache, core_packages, platform_packages): @cli.group("completion", short_help="Shell completion support") def completion(): - # pylint: disable=import-error,import-outside-toplevel - try: - import click_completion # pylint: disable=unused-import,unused-variable - except ImportError: - click.echo("Installing dependent packages...") - subprocess.check_call( - [proc.get_pythonexe_path(), "-m", "pip", "install", "click-completion"], - ) + pass @completion.command("install", short_help="Install shell completion files/code") -@click.option( - "--shell", - default=None, - type=click.Choice(["fish", "bash", "zsh", "powershell", "auto"]), - help="The shell type, default=auto", -) +@click.argument("shell", type=click.Choice([t.value for t in ShellType])) @click.option( "--path", type=click.Path(file_okay=True, dir_okay=False, readable=True, resolve_path=True), @@ -174,26 +162,18 @@ def completion(): "The standard installation path is used by default.", ) def completion_install(shell, path): - - import click_completion # pylint: disable=import-outside-toplevel,import-error - - shell = shell or click_completion.get_auto_shell() + shell = ShellType(shell) path = path or get_completion_install_path(shell) install_completion_code(shell, path) click.echo( "PlatformIO CLI completion has been installed for %s shell to %s \n" "Please restart a current shell session." - % (click.style(shell, fg="cyan"), click.style(path, fg="blue")) + % (click.style(shell.name, fg="cyan"), click.style(path, fg="blue")) ) @completion.command("uninstall", short_help="Uninstall shell completion files/code") -@click.option( - "--shell", - default=None, - type=click.Choice(["fish", "bash", "zsh", "powershell", "auto"]), - help="The shell type, default=auto", -) +@click.argument("shell", type=click.Choice([t.value for t in ShellType])) @click.option( "--path", type=click.Path(file_okay=True, dir_okay=False, readable=True, resolve_path=True), @@ -201,14 +181,11 @@ def completion_install(shell, path): "The standard installation path is used by default.", ) def completion_uninstall(shell, path): - - import click_completion # pylint: disable=import-outside-toplevel,import-error - - shell = shell or click_completion.get_auto_shell() + shell = ShellType(shell) path = path or get_completion_install_path(shell) uninstall_completion_code(shell, path) click.echo( "PlatformIO CLI completion has been uninstalled for %s shell from %s \n" "Please restart a current shell session." - % (click.style(shell, fg="cyan"), click.style(path, fg="blue")) + % (click.style(shell.name, fg="cyan"), click.style(path, fg="blue")) ) diff --git a/platformio/commands/system/completion.py b/platformio/commands/system/completion.py index 012df5fc..3ea80b99 100644 --- a/platformio/commands/system/completion.py +++ b/platformio/commands/system/completion.py @@ -13,61 +13,75 @@ # limitations under the License. import os -import subprocess +from enum import Enum import click +from platformio.compat import IS_MACOS + + +class ShellType(Enum): + FISH = "fish" + ZSH = "zsh" + BASH = "bash" + def get_completion_install_path(shell): home_dir = os.path.expanduser("~") prog_name = click.get_current_context().find_root().info_name - if shell == "fish": + if shell == ShellType.FISH: return os.path.join( home_dir, ".config", "fish", "completions", "%s.fish" % prog_name ) - if shell == "bash": - return os.path.join(home_dir, ".bash_completion") - if shell == "zsh": + if shell == ShellType.ZSH: return os.path.join(home_dir, ".zshrc") - if shell == "powershell": - return subprocess.check_output( - ["powershell", "-NoProfile", "echo $profile"] - ).strip() + if shell == ShellType.BASH: + return os.path.join(home_dir, ".bash_completion") + raise click.ClickException("%s is not supported." % shell) + + +def get_completion_code(shell): + if shell == ShellType.FISH: + return "eval (env _PIO_COMPLETE=fish_source pio)" + if shell == ShellType.ZSH: + code = "autoload -Uz compinit\ncompinit\n" if IS_MACOS else "" + return code + 'eval "$(_PIO_COMPLETE=zsh_source pio)"' + if shell == ShellType.BASH: + return 'eval "$(_PIO_COMPLETE=bash_source pio)"' raise click.ClickException("%s is not supported." % shell) def is_completion_code_installed(shell, path): - if shell == "fish" or not os.path.exists(path): + if shell == ShellType.FISH or not os.path.exists(path): return False - - import click_completion # pylint: disable=import-error,import-outside-toplevel - with open(path, encoding="utf8") as fp: - return click_completion.get_code(shell=shell) in fp.read() + return get_completion_code(shell) in fp.read() def install_completion_code(shell, path): - import click_completion # pylint: disable=import-error,import-outside-toplevel - if is_completion_code_installed(shell, path): return None - - return click_completion.install(shell=shell, path=path, append=shell != "fish") + append = shell != ShellType.FISH + with open(path, mode="a" if append else "w", encoding="utf8") as fp: + if append: + fp.write("\n\n# Begin: PlatformIO Core completion support\n") + fp.write(get_completion_code(shell)) + if append: + fp.write("\n# End: PlatformIO Core completion support\n\n") + return True def uninstall_completion_code(shell, path): if not os.path.exists(path): return True - if shell == "fish": + if shell == ShellType.FISH: os.remove(path) return True - import click_completion # pylint: disable=import-error,import-outside-toplevel - with open(path, "r+", encoding="utf8") as fp: contents = fp.read() fp.seek(0) fp.truncate() - fp.write(contents.replace(click_completion.get_code(shell=shell), "")) + fp.write(contents.replace(get_completion_code(shell), "")) return True diff --git a/setup.py b/setup.py index ac8c217e..7f0f92d4 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ from platformio.compat import PY2 minimal_requirements = [ "bottle==0.12.*", - "click>=7.1.2,<9,!=8.0.2", + "click>=8,<9,!=8.0.2", "colorama", "marshmallow%s" % (">=2,<3" if PY2 else ">=2,<4"), "pyelftools>=0.27,<1",