diff --git a/README.md b/README.md index 1aadf92..af971d1 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,20 @@ struct validate my-config.yaml struct mcp --server ``` +### Shell Completion + +Enable tab completion for struct commands and options: + +```sh +# Print exact setup commands for your shell (auto-detects if omitted) +struct completion install + +# Or specify explicitly +struct completion install zsh +struct completion install bash +struct completion install fish +``` + ### 🤖 MCP Integration Quick Start Struct supports MCP (Model Context Protocol) for seamless AI tool integration: diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 670aa44..0246a63 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -9,7 +9,7 @@ The `struct` CLI allows you to generate project structures from YAML configurati **Basic Usage:** ```sh -struct {info,validate,generate,list,generate-schema} ... +struct {info,validate,generate,list,generate-schema,mcp,completion} ... ``` ## Global Options @@ -107,6 +107,19 @@ struct generate-schema [-h] [-l LOG] [-c CONFIG_FILE] [-i LOG_FILE] [-s STRUCTUR - `-s STRUCTURES_PATH, --structures-path STRUCTURES_PATH`: Path to structure definitions. - `-o OUTPUT, --output OUTPUT`: Output file path for the schema (default: stdout). +### `completion` + +Manage shell completions for struct. + +Usage: + +```sh +struct completion install [bash|zsh|fish] +``` + +- If no shell is provided, the command attempts to auto-detect your current shell and prints the exact commands to enable argcomplete-based completion for struct. +- This does not modify your shell configuration; it only prints the commands you can copy-paste. + ## Examples ### Basic Structure Generation diff --git a/docs/completion.md b/docs/completion.md index 854ddde..a8cb7f8 100644 --- a/docs/completion.md +++ b/docs/completion.md @@ -7,6 +7,20 @@ STRUCT provides intelligent auto-completion for commands, options, and **structu ## Quick Setup +The easiest way is to ask struct to print the exact commands for your shell: + +```sh +# Auto-detect current shell and print install steps +struct completion install + +# Or specify explicitly +struct completion install zsh +struct completion install bash +struct completion install fish +``` + +You can still follow the manual steps below if you prefer. + For most users, this simple setup will enable full completion: ```sh diff --git a/struct_module/commands/completion.py b/struct_module/commands/completion.py new file mode 100644 index 0000000..cb7053b --- /dev/null +++ b/struct_module/commands/completion.py @@ -0,0 +1,60 @@ +from struct_module.commands import Command +import os + +SUPPORTED_SHELLS = ["bash", "zsh", "fish"] + +class CompletionCommand(Command): + def __init__(self, parser): + super().__init__(parser) + parser.description = "Manage CLI shell completions for struct (argcomplete)" + sub = parser.add_subparsers(dest="action") + + install = sub.add_parser("install", help="Print the commands to enable completion for your shell") + install.add_argument("shell", nargs="?", choices=SUPPORTED_SHELLS, help="Shell type (auto-detected if omitted)") + install.set_defaults(func=self._install) + + def _detect_shell(self): + shell = os.environ.get("SHELL", "") + if shell: + basename = os.path.basename(shell) + if basename in SUPPORTED_SHELLS: + return basename + # Fallback to zsh if running zsh, else bash + if os.environ.get("ZSH_NAME") or os.environ.get("ZDOTDIR"): + return "zsh" + return "bash" + + def _install(self, args): + shell = args.shell or self._detect_shell() + print(f"Detected shell: {shell}") + + if shell == "bash": + print("\n# One-time dependency (if not installed):") + print("python -m pip install argcomplete") + print("\n# Enable completion for 'struct' in bash (append to ~/.bashrc):") + print('echo "eval \"$(register-python-argcomplete struct)\"" >> ~/.bashrc') + print("\n# Apply now:") + print("source ~/.bashrc") + + elif shell == "zsh": + print("\n# One-time dependency (if not installed):") + print("python -m pip install argcomplete") + print("\n# Enable completion for 'struct' in zsh (append to ~/.zshrc):") + print('echo "eval \"$(register-python-argcomplete --shell zsh struct)\"" >> ~/.zshrc') + print("\n# Apply now:") + print("source ~/.zshrc") + + elif shell == "fish": + print("\n# One-time dependency (if not installed):") + print("python -m pip install argcomplete") + print("\n# Install fish completion file for 'struct':") + print('mkdir -p ~/.config/fish/completions') + print('register-python-argcomplete --shell fish struct > ~/.config/fish/completions/struct.fish') + print("\n# Apply now:") + print("fish -c 'source ~/.config/fish/completions/struct.fish'") + + else: + self.logger.error(f"Unsupported shell: {shell}. Supported: {', '.join(SUPPORTED_SHELLS)}") + return + + print("\nTip: If 'register-python-argcomplete' is not found, try:\n python -m argcomplete.shellintegration ") diff --git a/struct_module/main.py b/struct_module/main.py index a402c2e..cb72f90 100644 --- a/struct_module/main.py +++ b/struct_module/main.py @@ -31,6 +31,10 @@ def main(): GenerateSchemaCommand(subparsers.add_parser('generate-schema', help='Generate JSON schema for available structures')) MCPCommand(subparsers.add_parser('mcp', help='MCP (Model Context Protocol) support')) + # completion installer + from struct_module.commands.completion import CompletionCommand + CompletionCommand(subparsers.add_parser('completion', help='Manage shell completions')) + argcomplete.autocomplete(parser) args = parser.parse_args() diff --git a/tests/test_completion_command.py b/tests/test_completion_command.py new file mode 100644 index 0000000..43a68a0 --- /dev/null +++ b/tests/test_completion_command.py @@ -0,0 +1,61 @@ +import argparse +import os +from unittest.mock import patch + +from struct_module.commands.completion import CompletionCommand + + +def make_parser(): + return argparse.ArgumentParser() + + +def _gather_print_output(mock_print): + return "\n".join(str(call.args[0]) for call in mock_print.call_args_list) + + +def test_completion_install_bash_explicit(): + parser = make_parser() + cmd = CompletionCommand(parser) + with patch('builtins.print') as mock_print: + args = parser.parse_args(['install', 'bash']) + cmd._install(args) + out = _gather_print_output(mock_print) + assert "Detected shell: bash" in out + assert "register-python-argcomplete struct" in out + assert "~/.bashrc" in out + + +def test_completion_install_zsh_explicit(): + parser = make_parser() + cmd = CompletionCommand(parser) + with patch('builtins.print') as mock_print: + args = parser.parse_args(['install', 'zsh']) + cmd._install(args) + out = _gather_print_output(mock_print) + assert "Detected shell: zsh" in out + assert "register-python-argcomplete --shell zsh struct" in out + assert "~/.zshrc" in out + + +def test_completion_install_fish_explicit(): + parser = make_parser() + cmd = CompletionCommand(parser) + with patch('builtins.print') as mock_print: + args = parser.parse_args(['install', 'fish']) + cmd._install(args) + out = _gather_print_output(mock_print) + assert "Detected shell: fish" in out + assert "register-python-argcomplete --shell fish struct" in out + assert "~/.config/fish/completions/struct.fish" in out + + +def test_completion_install_auto_detect_zsh(): + parser = make_parser() + cmd = CompletionCommand(parser) + with patch.dict(os.environ, {"SHELL": "/bin/zsh"}, clear=False): + with patch('builtins.print') as mock_print: + args = parser.parse_args(['install']) + cmd._install(args) + out = _gather_print_output(mock_print) + assert "Detected shell: zsh" in out + assert "register-python-argcomplete --shell zsh struct" in out