Skip to content

Commit

Permalink
Dropped argcomplete and working on a standalone arg completion genera…
Browse files Browse the repository at this point in the history
…tion that better use Zsh features (#111).
  • Loading branch information
mindstorm38 committed Jan 28, 2024
1 parent ce45fd7 commit 4895498
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 74 deletions.
27 changes: 6 additions & 21 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 16 additions & 2 deletions portablemc/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
import socket
import sys

from .parse import register_arguments, RootNs, SearchNs, StartNs, LoginNs, LogoutNs, AuthBaseNs
from .parse import register_arguments, RootNs, SearchNs, StartNs, LoginNs, LogoutNs, AuthBaseNs, ShowCompletionNs
from .complete import gen_zsh_completion

from .util import format_locale_date, format_time, format_number, anonymize_email
from .output import Output, HumanOutput, MachineOutput, OutputTable
from .lang import get as _, lang
Expand Down Expand Up @@ -70,6 +72,7 @@ def main(args: Optional[List[str]] = None):
ns: RootNs = cast(RootNs, parser.parse_args(args or sys.argv[1:]))

# Setup common objects in the namespace.
ns.parser = parser
ns.out = get_output(ns.out_kind)
ns.context = Context(ns.main_dir, ns.work_dir)
ns.version_manifest = VersionManifest(ns.context.work_dir / MANIFEST_CACHE_FILE_NAME)
Expand Down Expand Up @@ -124,6 +127,7 @@ def get_command_handlers() -> CommandTree:
"about": cmd_show_about,
"auth": cmd_show_auth,
"lang": cmd_show_lang,
"completion": cmd_show_completion,
},
}

Expand Down Expand Up @@ -310,7 +314,7 @@ def cmd_start(ns: StartNs):
version.disable_chat = ns.disable_chat
version.demo = ns.demo
version.resolution = ns.resolution
version.jvm_path = None if ns.jvm is None else Path(ns.jvm)
version.jvm_path = ns.jvm

if ns.server is not None:
version.set_quick_play_multiplayer(ns.server, ns.server_port or 25565)
Expand Down Expand Up @@ -538,6 +542,16 @@ def cmd_show_lang(ns: RootNs):
table.print()


def cmd_show_completion(ns: ShowCompletionNs):

if ns.shell == "zsh":
content = gen_zsh_completion(ns.parser)
else:
raise RuntimeError

print(content, end="")


def prompt_authenticate(ns: AuthBaseNs, email: str, caching: bool, anonymise: bool = False) -> Optional[AuthSession]:
"""Prompt the user to login using the given email (or legacy username) for specific
service (Microsoft or Yggdrasil) and return the :class:`AuthSession` if successful,
Expand Down
132 changes: 132 additions & 0 deletions portablemc/cli/complete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
from io import StringIO
from argparse import ArgumentParser, \
_CountAction, _StoreAction, _SubParsersAction, \
_StoreConstAction, _HelpAction, _AppendConstAction

from .lang import get as _
from .parse import type_path, type_path_dir, \
type_email_or_username, type_host, get_completions

from typing import Dict, Tuple, cast


def escape_zsh(s: str, *, space = False) -> str:
s = s.replace("'", "''").replace("[", "\\[").replace("]", "\\]").replace(":", "\\:")
if space:
s = s.replace(" ", "\\ ")
return s


def gen_zsh_completion(parser: ArgumentParser) -> str:
buffer = StringIO()
gen_zsh_parser_completion(parser, buffer, "_complete_portablemc")
buffer.write("compdef _complete_portablemc portablemc\n")
return buffer.getvalue()


def gen_zsh_parser_completion(parser: ArgumentParser, buffer: StringIO, function: str):

commands: Dict[str, Tuple[str, ArgumentParser]] = {}
completions: Dict[str, Dict[str, str]] = {}

buffer.write(function)
buffer.write("() {\n")
buffer.write(" local curcontext=$curcontext state line\n")
buffer.write(" integer ret=1\n")

buffer.write(" _arguments -s -C \\\n")

for action in parser._actions:

zsh_description = escape_zsh(action.help or "")
zsh_repeat = ""
zsh_action = ": :"

# Depending on the action type there are some specific things we can do.
if isinstance(action, _CountAction):
zsh_repeat = "\\*"
zsh_action = ""
elif isinstance(action, _StoreAction):

action_completions = get_completions(action)
if action.choices is not None:
for choice in action.choices:
if choice not in action_completions:
action_completions[choice] = ""

if action.type == type_path:
zsh_action = ": :_files"
elif action.type == type_path_dir:
zsh_action = ": :_files -/"
elif action.type == type_email_or_username:
zsh_action = ": :_email_addresses -c"
elif action.type == type_host:
zsh_action = ": :_hosts"
elif len(action_completions):
zsh_action = f": :->action_{action.dest}"
completions[f"action_{action.dest}"] = action_completions

elif isinstance(action, (_HelpAction, _StoreConstAction, _AppendConstAction)):
zsh_action = ""
elif isinstance(action, _SubParsersAction):
parsers_choices = cast(Dict[str, ArgumentParser], action.choices)
for sub_action in action._get_subactions():
commands[sub_action.dest] = (sub_action.help or "", parsers_choices[sub_action.dest])
continue

# If the argument is positional.
if not len(action.option_strings):
buffer.write(f" '{zsh_action}' \\\n")
continue

# If the argument is an option.
if len(action.option_strings) > 1:
zsh_names = f"{{{','.join(action.option_strings)}}}"
else:
zsh_names = action.option_strings[0]
buffer.write(f" {zsh_repeat}{zsh_names}'[{zsh_description}]{zsh_action}' \\\n")

if len(commands):
buffer.write(" ': :->command' \\\n")
buffer.write(" '*:: :->option' \\\n")

buffer.write(" && ret=0\n")

if len(commands) or len(completions):

buffer.write(" case $state in\n")

if len(commands):
buffer.write(" command)\n")
buffer.write(" local -a commands=(\n")
for name, (description, parser) in commands.items():
buffer.write(f" '{name}:{escape_zsh(description)}'\n")
buffer.write(" )\n")
buffer.write(" _describe -t commands command commands && ret=0\n")
buffer.write(" ;;\n")

buffer.write(" option)\n")
buffer.write(" case $line[1] in\n")
for name, (description, parser) in commands.items():
buffer.write(f" {name}) {function}_{name} ;;\n")
buffer.write(" esac\n")
buffer.write(" ;;\n")

for state, action_completions in completions.items():
buffer.write(f" {state})\n")
buffer.write(" local -a completions=(\n")
for name, description in action_completions.items():
if len(description):
buffer.write(f" '{escape_zsh(name)}:{escape_zsh(description)}'\n")
else:
buffer.write(f" '{escape_zsh(name)}'\n")
buffer.write(" )\n")
buffer.write(" _describe -t values value completions && ret=0\n")
buffer.write(" ;;\n")

buffer.write(" esac\n")

buffer.write("}\n\n")

for name, (description, parser) in commands.items():
gen_zsh_parser_completion(parser, buffer, f"{function}_{name}")
24 changes: 23 additions & 1 deletion portablemc/cli/lang.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,27 @@ def get(key: str, **kwargs) -> str:
"runtime binaries and authentication.",
"args.timeout": "Set a global timeout (in decimal seconds) for network requests.",
"args.output": "Set the output format of the launcher, defaults to human-color, human if not a TTY.",
"args.output.comp.human-color": "Human readable output with color.",
"args.output.comp.human": "Human readable output.",
"args.output.comp.machine": "Machine readable output.",
"args.verbose": "Enable verbose output. The more -v argument you put, the more verbose the launcher will be, depending on subcommands' support (usually -v, -vv, -vvv).",
# Args common langs
"args.common.help": "Show this help message and exit.",
"args.common.auth_service": "Authentication service type to use for logging in the game.",
"args.common.auth_service.comp.microsoft": "Microsoft authentication (default).",
"args.common.auth_service.comp.yggdrasil": "Mojang authentication (deprecated).",
"args.common.auth_no_browser": "Prevent the authentication service to open your system's web browser.",
# Args search
"args.search": "Search for Minecraft versions.",
"args.search.kind": "Select the kind of search to operate.",
"args.search.kind.comp.mojang": "Search for official Mojang versions (default).",
"args.search.kind.comp.local": "Search for locally installed versions.",
"args.search.kind.comp.forge": "Search for Forge versions.",
"args.search.kind.comp.fabric": "Search for Fabric versions.",
"args.search.kind.comp.quilt": "Search for Quilt versions.",
"args.search.input": "Search input.",
"args.search.input.comp.release": "Resolve version of the latest release.",
"args.search.input.comp.snapshot": "Resolve version of the latest snapshot.",
# Args start
"args.start": "Start a Minecraft version.",
"args.start.version": "Version identifier (default to release): {formats}.",
Expand All @@ -61,6 +73,12 @@ def get(key: str, **kwargs) -> str:
"args.start.version.quilt": "quilt:[<vanilla-version>[:<loader-version>]]",
"args.start.version.forge": "forge:[<forge-version>] (forge-version >= 1.5.2)",
"args.start.version.neoforge": "neoforge:[<neoforge-version>] (neoforge-version >= 1.20.1)",
"args.start.version.comp.release": "Start the latest release (default).",
"args.start.version.comp.snapshot": "Start the latest snapshot.",
"args.start.version.comp.fabric": "Start Fabric mod loader with latest release.",
"args.start.version.comp.quilt": "Start Quilt mod loader with latest release.",
"args.start.version.comp.forge": "Start Forge mod loader with latest release.",
"args.start.version.comp.neoforge": "Start NeoForge mod loader with latest release.",
"args.start.dry": "Simulate game starting.",
"args.start.disable_multiplayer": "Disable the multiplayer buttons (>= 1.16).",
"args.start.disable_chat": "Disable the online chat (>= 1.16).",
Expand Down Expand Up @@ -94,7 +112,7 @@ def get(key: str, **kwargs) -> str:
"args.start.username": "Set a custom user name to play.",
"args.start.uuid": "Set a custom user UUID to play.",
"args.start.server": "Start the game and directly connect to a multiplayer server (>= 1.6).",
"args.start.server_port": "Set the server address port (given with -s, --server, >= 1.6).",
"args.start.server_port": "Set the server port (given with -s, --server, >= 1.6).",
# Args login
"args.login": "Login into your account and save the session.",
"args.login.microsoft": "Login using Microsoft account.",
Expand All @@ -106,6 +124,10 @@ def get(key: str, **kwargs) -> str:
"args.show.about": "Display authors, version and license of PortableMC.",
"args.show.auth": "Debug the authentication database and supported services.",
"args.show.lang": "Debug the language mappings used for messages translation.",
"args.show.completion": "Print a shell completion script.",
"args.show.completion.shell": "The shell to generate completion script for (default to your current shell).",
"args.show.completion.shell.comp.zsh": "Generate completion script for Zsh.",
"args.show.completion.shell.comp.bash": "Generate completion script for Bash.",
# Common
"echo": "{echo}",
"cancelled": "Cancelled.",
Expand Down
Loading

0 comments on commit 4895498

Please sign in to comment.