From 134d0fdf3052377f62605724dcd91409617ddc22 Mon Sep 17 00:00:00 2001 From: Kenneth Belitzky Date: Mon, 3 Aug 2026 20:49:30 -0300 Subject: [PATCH] Fix Fish shell completions --- docs/completion.md | 6 +- structkit/commands/completion.py | 4 +- structkit/main.py | 99 +++++++++++++++++++++++++++++++- tests/test_completion_command.py | 18 ++++++ 4 files changed, 118 insertions(+), 9 deletions(-) diff --git a/docs/completion.md b/docs/completion.md index b0723c1..8ac6d3d 100644 --- a/docs/completion.md +++ b/docs/completion.md @@ -1,6 +1,6 @@ # Command-Line Auto-Completion -StructKit provides intelligent auto-completion for commands, options, and structure names using static completion scripts generated by [shtab](https://github.com/Iterative/shtab). This approach is reliable across shells and doesn’t require runtime hooks or markers. +StructKit provides intelligent auto-completion for commands, options, and structure names using static completion scripts. Bash, Zsh, and Tcsh scripts are generated by [shtab](https://github.com/Iterative/shtab); Fish scripts are generated by StructKit. This approach is reliable across shells and doesn’t require runtime hooks or markers. !!! tip "Structure Name Completion" StructKit completes structure names when using `structkit generate`, showing available structures from both built-in and custom paths. @@ -19,11 +19,11 @@ structkit completion install bash structkit completion install fish ``` -You can also generate completion files manually with shtab as shown below. +You can also generate completion files manually as shown below. ## Manual Installation -### 1) Install shtab +### 1) Install shtab (Bash, Zsh, and Tcsh only) ```sh pip install shtab diff --git a/structkit/commands/completion.py b/structkit/commands/completion.py index 7b95065..a94fa96 100644 --- a/structkit/commands/completion.py +++ b/structkit/commands/completion.py @@ -51,9 +51,7 @@ def _install(self, args): print("exec zsh") elif shell == "fish": - print("\n# Install shtab (once, in your environment):") - print("python -m pip install shtab") - print("\n# Generate static fish completion for 'struct':") + print("\n# Generate static fish completion for 'structkit':") print('mkdir -p ~/.config/fish/completions') print('structkit --print-completion fish > ~/.config/fish/completions/structkit.fish') print("\n# Apply now:") diff --git a/structkit/main.py b/structkit/main.py index 3493199..401dfdf 100644 --- a/structkit/main.py +++ b/structkit/main.py @@ -1,6 +1,7 @@ import argparse import logging import os +import shlex from dotenv import load_dotenv from structkit.utils import read_config_file, merge_configs from structkit.commands.generate import GenerateCommand @@ -24,6 +25,94 @@ shtab = None load_dotenv() +SUPPORTED_COMPLETION_SHELLS = ("bash", "zsh", "tcsh", "fish") + + +def _fish_quote(value): + return shlex.quote(str(value)) + + +def _fish_condition(command_path): + if not command_path: + return "__fish_structkit_using_command" + return "__fish_structkit_using_command " + " ".join(_fish_quote(command) for command in command_path) + + +def _fish_completion_script(parser): + """Generate a static Fish completion script from an argparse parser.""" + lines = [ + "function __fish_structkit_using_command", + " set -l words (commandline -opc)", + " set -e words[1]", + " set -l commands", + " for word in $words", + " if not string match -qr '^-' -- $word", + " set -a commands $word", + " end", + " end", + " test (count $commands) -eq (count $argv)", + " or return 1", + " for index in (seq (count $argv))", + " test \"$commands[$index]\" = \"$argv[$index]\"", + " or return 1", + " end", + "end", + "", + f"complete -c {_fish_quote(parser.prog)} -f", + ] + + def add_completions(current_parser, command_path=()): + condition = _fish_condition(command_path) + for action in current_parser._actions: + if isinstance(action, argparse._SubParsersAction): + for name, subparser in action.choices.items(): + if name not in action._name_parser_map: + continue + help_text = action._name_parser_map[name].description or "" + command = ( + f"complete -c {_fish_quote(parser.prog)} -n {_fish_quote(condition)} " + f"-a {_fish_quote(name)}" + ) + if help_text: + command += f" -d {_fish_quote(help_text)}" + lines.append(command) + add_completions(subparser, command_path + (name,)) + continue + + if action.help is argparse.SUPPRESS: + continue + + choices = getattr(action, "choices", None) + arguments = "" + if choices: + arguments = " -a " + _fish_quote(" ".join(map(str, choices))) + + for option in action.option_strings: + option_type = "-l" if option.startswith("--") else "-s" + command = ( + f"complete -c {_fish_quote(parser.prog)} -n {_fish_quote(condition)} " + f"{option_type} {_fish_quote(option.lstrip('-'))}" + ) + if action.nargs != 0: + command += " -r" + command += arguments + if action.help: + command += f" -d {_fish_quote(action.help)}" + lines.append(command) + + add_completions(parser) + return "\n".join(lines) + + +class PrintCompletionAction(argparse.Action): + """Print a completion script for shtab-supported shells or Fish.""" + + def __call__(self, parser, namespace, values, option_string=None): + if values == "fish": + print(_fish_completion_script(parser)) + else: + print(shtab.complete(parser, values)) + parser.exit(0) def get_parser(): @@ -57,10 +146,14 @@ def get_parser(): from structkit.commands.completion import CompletionCommand CompletionCommand(subparsers.add_parser('completion', help='Manage shell completions')) - # Add shtab completion printing flags if available + # shtab supports bash, zsh, and tcsh. Fish is generated locally. if shtab is not None: - # Adds --print-completion and --shell flags - shtab.add_argument_to(parser) + parser.add_argument( + "--print-completion", + choices=SUPPORTED_COMPLETION_SHELLS, + action=PrintCompletionAction, + help="print shell completion script", + ) return parser diff --git a/tests/test_completion_command.py b/tests/test_completion_command.py index f215fb1..f5a0b72 100644 --- a/tests/test_completion_command.py +++ b/tests/test_completion_command.py @@ -3,6 +3,7 @@ from unittest.mock import patch from structkit.commands.completion import CompletionCommand +from structkit.main import get_parser def make_parser(): @@ -47,6 +48,7 @@ def test_completion_install_fish_explicit(): assert "Detected shell: fish" in out assert "structkit --print-completion fish" in out assert "~/.config/fish/completions/structkit.fish" in out + assert "pip install shtab" not in out def test_completion_install_auto_detect_zsh(): @@ -59,3 +61,19 @@ def test_completion_install_auto_detect_zsh(): out = _gather_print_output(mock_print) assert "Detected shell: zsh" in out assert "structkit --print-completion zsh" in out + + +def test_print_completion_fish_generates_a_fish_script(capsys): + parser = get_parser() + + try: + parser.parse_args(['--print-completion', 'fish']) + except SystemExit as error: + assert error.code == 0 + + output = capsys.readouterr().out + assert "function __fish_structkit_using_command" in output + assert "complete -c structkit -f" in output + assert "complete -c structkit -n __fish_structkit_using_command -a generate" in output + assert " -s h -d " in output + assert " -l print-completion -r -a 'bash zsh tcsh fish'" in output