diff --git a/poetry.lock b/poetry.lock index 90668025345..1e2b943531d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -14,6 +14,20 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "argcomplete" +version = "2.0.0" +description = "Bash tab completion for argparse" +category = "main" +optional = true +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = ">=0.23,<5", markers = "python_version == \"3.7\""} + +[package.extras] +test = ["coverage", "flake8", "pexpect", "wheel"] + [[package]] name = "attrs" version = "22.1.0" @@ -250,7 +264,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "importlib-metadata" version = "4.13.0" description = "Read metadata from Python packages" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" @@ -970,7 +984,7 @@ python-versions = "*" name = "typing-extensions" version = "4.4.0" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" @@ -1002,7 +1016,7 @@ watchmedo = ["PyYAML (>=3.10)"] name = "zipp" version = "3.9.0" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" @@ -1011,6 +1025,7 @@ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [extras] +completion = ["argcomplete"] coverage = [] docs = [] format = [] @@ -1020,7 +1035,7 @@ test = [] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "44f3d12d61307a3b9811ff20816c0860900e29399af5d0dd5823d28dd65b5ded" +content-hash = "c8d56d52067a52277751d1746b9783bb39a5e3735be989acf80d7eee8e875813" [metadata.files] aafigure = [ @@ -1031,6 +1046,10 @@ alabaster = [ {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, ] +argcomplete = [ + {file = "argcomplete-2.0.0-py2.py3-none-any.whl", hash = "sha256:cffa11ea77999bb0dd27bb25ff6dc142a6796142f68d45b1a26b11f58724561e"}, + {file = "argcomplete-2.0.0.tar.gz", hash = "sha256:6372ad78c89d662035101418ae253668445b391755cfe94ea52f1b9d22425b20"}, +] attrs = [ {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, diff --git a/pyproject.toml b/pyproject.toml index 02ce57a30af..6699056c412 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,7 @@ python = "^3.7" libtmux = "~0.15.8" colorama = ">=0.3.9" PyYAML = "^6.0" +argcomplete = { version = "^2.0.0", optional = true } [tool.poetry.dev-dependencies] ### Docs ### @@ -98,6 +99,7 @@ types-PyYAML = "*" importlib-metadata = "<5" # https://github.com/PyCQA/flake8/issues/1701 [tool.poetry.extras] +completion = ["argcomplete"] docs = [ "docutils", "sphinx", @@ -158,6 +160,7 @@ files = [ [[tool.mypy.overrides]] module = [ "shtab", + "argcomplete.*", "aafigure", "IPython.*", "ptpython.*", diff --git a/src/tmuxp/cli/__init__.py b/src/tmuxp/cli/__init__.py index e676bed51d9..c880e38200f 100644 --- a/src/tmuxp/cli/__init__.py +++ b/src/tmuxp/cli/__init__.py @@ -93,6 +93,13 @@ def create_parser() -> argparse.ArgumentParser: ) create_freeze_subparser(freeze_parser) + try: + import argcomplete + + argcomplete.autocomplete(parser) + except ImportError: + pass + return parser @@ -123,6 +130,13 @@ def cli(_args: t.Optional[t.List[str]] = None) -> None: sys.exit() parser = create_parser() + + try: + import argcomplete + + argcomplete.autocomplete(parser) + except ImportError: + pass args = parser.parse_args(_args, namespace=ns) setup_logger(logger=logger, level=args.log_level.upper()) diff --git a/src/tmuxp/cli/completions.py b/src/tmuxp/cli/completions.py new file mode 100644 index 00000000000..1b050094ae2 --- /dev/null +++ b/src/tmuxp/cli/completions.py @@ -0,0 +1,101 @@ +import os +import pathlib +import typing as t + +from libtmux.server import Server +from tmuxp import config +from tmuxp.cli.utils import get_config_dir + +config_dir = get_config_dir() + +try: + import argcomplete + import argcomplete.completers +except ImportError: + + class ArgComplete: + class Completers: + class Completer: + def __call__(self, *args: object, **kwargs: object) -> object: + ... + + FilesCompleter = Completer + + completers = Completers() + + argcomplete = ArgComplete() # type:ignore + + +class ConfigFileCompleter(argcomplete.completers.FilesCompleter): + """argcomplete completer for tmuxp files.""" + + def __init__( + self, + allowednames: t.Sequence[str] = ("yml", "yaml", "json"), + directories: bool = False, + **kwargs: object + ): + super().__init__(allowednames=allowednames, directories=directories, **kwargs) + + def __call__(self, prefix: str, **kwargs): + completion: t.List[str] = super().__call__(prefix, **kwargs) + completion.extend([pathlib.Path(c).stem for c in config.in_dir(config_dir)]) + + return completion + + +class TmuxinatorCompleter(argcomplete.completers.FilesCompleter): + """argcomplete completer for Tmuxinator files.""" + + def __call__(self, prefix, **kwargs): + from tmuxp.cli.import_config import get_tmuxinator_dir + + tmuxinator_config_dir = get_tmuxinator_dir() + completion = argcomplete.completers.FilesCompleter.__call__( + self, prefix, **kwargs + ) + tmuxinator_configs = config.in_dir(tmuxinator_config_dir, extensions="yml") + completion += [ + os.path.join(tmuxinator_config_dir, f) for f in tmuxinator_configs + ] + + return completion + + +class TeamocilCompleter(argcomplete.completers.FilesCompleter): + + """argcomplete completer for Teamocil files.""" + + def __call__(self, prefix, **kwargs): + from tmuxp.cli.import_config import get_teamocil_dir + + teamocil_config_dir = get_teamocil_dir() + + completion = argcomplete.completers.FilesCompleter.__call__( + self, prefix, **kwargs + ) + teamocil_configs = config.in_dir(teamocil_config_dir, extensions="yml") + completion += [os.path.join(teamocil_config_dir, f) for f in teamocil_configs] + + return completion + + +def SessionCompleter(prefix, parsed_args, **kwargs): + """Return list of session names for argcomplete completer.""" + + t = Server(socket_name=parsed_args.socket_name, socket_path=parsed_args.socket_path) + + sessions_available = [ + s.get("session_name") + for s in t._sessions + if s.get("session_name").startswith(" ".join(prefix)) + ] + + if parsed_args.session_name and sessions_available: + return [] + + return [ + s.get("session_name") + for s in t._sessions + if s.get("session_name").startswith(prefix) + ] diff --git a/src/tmuxp/cli/convert.py b/src/tmuxp/cli/convert.py index be9548004f5..cd82739bc8d 100644 --- a/src/tmuxp/cli/convert.py +++ b/src/tmuxp/cli/convert.py @@ -18,9 +18,9 @@ def create_convert_subparser( help="checks tmuxp and current directory for config files.", ) try: - import shtab + from tmuxp.cli.completions import ConfigFileCompleter - config_file.complete = shtab.FILE # type: ignore + config_file.completer = ConfigFileCompleter() # type:ignore except ImportError: pass diff --git a/src/tmuxp/cli/import_config.py b/src/tmuxp/cli/import_config.py index 14a40076ce7..4d26ef94d9a 100644 --- a/src/tmuxp/cli/import_config.py +++ b/src/tmuxp/cli/import_config.py @@ -105,10 +105,15 @@ def create_import_subparser( ) try: - import shtab + import argcomplete + import argcomplete.completers - teamocil_config_file.complete = shtab.FILE # type: ignore - tmuxinator_config_file.complete = shtab.FILE # type: ignore + teamocil_config_file.completer = ( # type: ignore + argcomplete.completers.FilesCompleter() + ) + tmuxinator_config_file.completer = ( # type:ignore + argcomplete.completers.FilesCompleter() + ) except ImportError: pass diff --git a/src/tmuxp/cli/load.py b/src/tmuxp/cli/load.py index ec016c9fa9a..4cee1434597 100644 --- a/src/tmuxp/cli/load.py +++ b/src/tmuxp/cli/load.py @@ -566,11 +566,16 @@ def create_load_subparser(parser: argparse.ArgumentParser) -> argparse.ArgumentP ) try: - import shtab + import argcomplete + import argcomplete.completers - config_file.complete = shtab.FILE # type: ignore - tmux_config_file.complete = shtab.FILE # type: ignore - log_file.complete = shtab.FILE # type: ignore + from tmuxp.cli.completions import ConfigFileCompleter + + config_file.completer = ConfigFileCompleter() # type: ignore + tmux_config_file.completer = ( # type: ignore + argcomplete.completers.FilesCompleter() + ) + log_file.completer = argcomplete.completers.FilesCompleter() # type: ignore except ImportError: pass