-
-
Notifications
You must be signed in to change notification settings - Fork 9
Enable Zsh completions #34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
j-g00da
wants to merge
20
commits into
pawamoy:main
Choose a base branch
from
j-g00da:zsh-completions
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
ef0e737
WIP: zsh completion
j-g00da 57499a5
Option to set shell with --completion argument
j-g00da 65d6737
Shell completions - don't use a fallback shell as this can be mislead…
j-g00da 682e301
Update src/duty/cli.py
j-g00da 1520402
Remove an unnecessary COMPLETION_DIR, change completion error message.
j-g00da 338de7c
WIP: Use compdef for richer completions and improve docs
j-g00da 592f938
WIP: Zsh completions with descriptions
j-g00da 6b08ffe
WIP: --install-completion flag
j-g00da e3931a0
Update src/duty/completion.py
j-g00da 1db233c
Improve shell completions implementation
j-g00da eed6bdf
Merge branch 'main' of github.com:j-g00da/duty into zsh-completions
j-g00da a0e7dbd
Remove leftover prints
j-g00da 59b20e1
Fail if no bash-completion directory; Improve error messages
j-g00da f04950b
Fix quotes
j-g00da 4179b07
Rewrite symlinks instead of informing the user that completions are a…
j-g00da 2cb10e1
Don't single-quote symlink_path
j-g00da 71a8626
Minor improvements
j-g00da 6daed95
WIP: Tests - bash
j-g00da 57f101d
WIP: Tests
j-g00da c8db8ea
WIP: remove pytest isolate marker
j-g00da File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| """Shell completion utilities.""" | ||
|
|
||
| import os | ||
| import subprocess | ||
| import sys | ||
| from pathlib import Path | ||
| from typing import Optional | ||
|
|
||
| CompletionCandidateType = tuple[str, Optional[str]] | ||
|
|
||
|
|
||
| class CompletionParser: | ||
| """Shell completion parser.""" | ||
|
|
||
| @classmethod | ||
| def parse(cls, candidates: list[CompletionCandidateType], shell: str) -> str: | ||
| """Parses a list of completion candidates for the selected shell's completion command. | ||
|
|
||
| Parameters: | ||
| candidates: List of completion candidates with optional descriptions. | ||
| shell: Shell for which to parse the candidates. | ||
|
|
||
| Raises: | ||
| NotImplementedError: When parser is not implemented for selected shell. | ||
|
|
||
| Returns: | ||
| String to be passed to shell completion command. | ||
| """ | ||
| try: | ||
| return getattr(cls, f"_{shell}")(candidates) | ||
| except AttributeError as exc: | ||
| msg = f"Completion parser method for {shell!r} shell is not implemented!" | ||
| raise NotImplementedError(msg) from exc | ||
|
|
||
| @staticmethod | ||
| def _zsh(candidates: list[CompletionCandidateType]) -> str: | ||
| def parse_candidate(item: CompletionCandidateType) -> str: | ||
| completion, help_text = item | ||
| # We only have space for one line of description, | ||
| # so we remove descriptions of sub-command parameters from help_text | ||
| # by removing everything after the first newline. | ||
| return f"{completion}: {help_text or '-'}".split("\n", 1)[0] | ||
|
|
||
| return "\n".join(parse_candidate(candidate) for candidate in candidates) | ||
|
|
||
| @staticmethod | ||
| def _bash(candidates: list[CompletionCandidateType]) -> str: | ||
| return "\n".join(completion for completion, _ in candidates) | ||
j-g00da marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
|
|
||
| class CompletionInstaller: | ||
| """Shell completion installer.""" | ||
|
|
||
| @classmethod | ||
| def install(cls, shell: str) -> None: | ||
| """Installs shell completions for selected shell. | ||
|
|
||
| Raises: | ||
| NotImplementedError: When installer is not implemented for selected shell. | ||
| """ | ||
| try: | ||
| return getattr(cls, f"_{shell}")() | ||
j-g00da marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| except AttributeError as exc: | ||
| msg = f"Completion installer method for {shell!r} shell is not implemented!" | ||
| raise NotImplementedError(msg) from exc | ||
|
|
||
| @staticmethod | ||
| def get_completion_script_path(shell: str) -> Path: | ||
| """Gets the path of a shell completion script for the selected shell.""" | ||
| completions_file_path = Path(__file__).parent / f"completions.{shell}" | ||
| if not completions_file_path.exists(): | ||
| msg = f"Completions for {shell!r} shell are not available, feature requests and PRs welcome!" | ||
| raise NotImplementedError(msg) | ||
| return completions_file_path | ||
|
|
||
| @classmethod | ||
| def _zsh(cls) -> None: | ||
| site_functions_dirs = (Path("/usr/local/share/zsh/site-functions"), Path("/usr/share/zsh/site-functions")) | ||
| try: | ||
| completions_dir = next(d for d in site_functions_dirs if d.is_dir()) | ||
| except StopIteration as exc: | ||
| raise OSError("Zsh site-functions directory not found!") from exc | ||
|
|
||
| try: | ||
| symlink_path = completions_dir / "_duty" | ||
| symlink_path.symlink_to(cls.get_completion_script_path("zsh")) | ||
| except PermissionError: | ||
| # retry as sudo | ||
| if os.geteuid() == 0: | ||
| raise | ||
| subprocess.run( # noqa: S603 | ||
| ["sudo", sys.executable, sys.argv[0], "--install-completion=zsh"], # noqa: S607 | ||
| check=True, | ||
| ) | ||
| except FileExistsError: | ||
| print("Zsh completions already installed.") | ||
j-g00da marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| else: | ||
| print( | ||
| f"Zsh completions successfully symlinked to {symlink_path}. " | ||
| f"Please reload Zsh for changes to take effect.", | ||
| ) | ||
|
|
||
| @classmethod | ||
| def _bash(cls) -> None: | ||
| bash_completion_user_dir = os.environ.get("BASH_COMPLETION_USER_DIR") | ||
| xdg_data_home = os.environ.get("XDG_DATA_HOME") | ||
|
|
||
| if bash_completion_user_dir: | ||
| completion_dir = Path(bash_completion_user_dir) / "completions" | ||
| elif xdg_data_home: | ||
| completion_dir = Path(xdg_data_home) / "bash-completion/completions" | ||
| else: | ||
| completion_dir = Path.home() / ".local/share/bash-completion/completions" | ||
|
|
||
| completion_dir.mkdir(parents=True, exist_ok=True) | ||
| symlink_path = completion_dir / "duty" | ||
| try: | ||
| symlink_path.symlink_to(cls.get_completion_script_path("bash")) | ||
| except FileExistsError: | ||
| print("Bash completions already installed.") | ||
| else: | ||
| print( | ||
| f"Bash completions successfully symlinked to {symlink_path!r}. " | ||
| f"Please reload Bash for changes to take effect.", | ||
| ) | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.