ixt-cli 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ixt/__init__.py +8 -0
- ixt/__main__.py +8 -0
- ixt/backends/__init__.py +1 -0
- ixt/backends/binary.py +935 -0
- ixt/backends/binary_resolver.py +307 -0
- ixt/backends/node.py +490 -0
- ixt/backends/python.py +234 -0
- ixt/cli/__init__.py +31 -0
- ixt/cli/argparse_completion.py +557 -0
- ixt/cli/cmd_apply.py +404 -0
- ixt/cli/cmd_cache.py +86 -0
- ixt/cli/cmd_config.py +295 -0
- ixt/cli/cmd_info.py +116 -0
- ixt/cli/cmd_install.py +508 -0
- ixt/cli/cmd_misc.py +261 -0
- ixt/cli/cmd_registry.py +35 -0
- ixt/cli/cmd_upgrade.py +336 -0
- ixt/cli/commands.py +70 -0
- ixt/cli/parser.py +555 -0
- ixt/cli/render.py +85 -0
- ixt/config/__init__.py +5 -0
- ixt/config/asset_index.py +305 -0
- ixt/config/asset_pattern_cache.py +87 -0
- ixt/config/env_policy.py +340 -0
- ixt/config/flags.py +29 -0
- ixt/config/fs_policy.py +17 -0
- ixt/config/heuristics.py +465 -0
- ixt/config/models.py +176 -0
- ixt/config/registry.py +145 -0
- ixt/config/settings.py +173 -0
- ixt/config/setup_toml.py +179 -0
- ixt/config/toml.py +416 -0
- ixt/core/__init__.py +16 -0
- ixt/core/apply.py +564 -0
- ixt/core/apply_actions.py +106 -0
- ixt/core/backend.py +187 -0
- ixt/core/bootstrap.py +410 -0
- ixt/core/cache.py +332 -0
- ixt/core/discover.py +150 -0
- ixt/core/doctor.py +591 -0
- ixt/core/export.py +419 -0
- ixt/core/expose.py +350 -0
- ixt/core/extract.py +261 -0
- ixt/core/hooks.py +182 -0
- ixt/core/identity.py +148 -0
- ixt/core/inject.py +143 -0
- ixt/core/install.py +509 -0
- ixt/core/install_local.py +229 -0
- ixt/core/locks.py +54 -0
- ixt/core/pathlink.py +86 -0
- ixt/core/resolution_stats.py +191 -0
- ixt/core/resolve.py +150 -0
- ixt/core/resolve_cache.py +185 -0
- ixt/core/runtimes.py +192 -0
- ixt/core/save.py +237 -0
- ixt/core/setup_completions.py +11 -0
- ixt/core/setup_path.py +368 -0
- ixt/core/upgrade.py +596 -0
- ixt/data/__init__.py +10 -0
- ixt/data/asset_index.json +574 -0
- ixt/data/heuristics.toml +98 -0
- ixt/data/registry.toml +71 -0
- ixt/libs/__init__.py +3 -0
- ixt/libs/constants.py +4 -0
- ixt/libs/fmt.py +108 -0
- ixt/libs/logger.py +109 -0
- ixt/libs/output.py +25 -0
- ixt/libs/req_spec.py +115 -0
- ixt/libs/semver.py +149 -0
- ixt/libs/shell.py +126 -0
- ixt/libs/style.py +238 -0
- ixt/net/__init__.py +1 -0
- ixt/net/github_api.py +158 -0
- ixt/net/gitlab_api.py +149 -0
- ixt/net/http.py +194 -0
- ixt/net/npm.py +24 -0
- ixt/net/pypi.py +26 -0
- ixt/net/source.py +163 -0
- ixt/platform/__init__.py +131 -0
- ixt/platform/win.py +68 -0
- ixt_cli-0.8.0.dist-info/METADATA +294 -0
- ixt_cli-0.8.0.dist-info/RECORD +84 -0
- ixt_cli-0.8.0.dist-info/WHEEL +4 -0
- ixt_cli-0.8.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
"""Shell completion generation from an ``argparse`` parser.
|
|
2
|
+
|
|
3
|
+
The parser remains the source of truth for commands, options, and choices.
|
|
4
|
+
This module extracts the small subset of argparse metadata needed to render
|
|
5
|
+
shell scripts without adding a third-party dependency.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
SUPPORTED_COMPLETION_SHELLS = ("bash", "zsh", "fish")
|
|
15
|
+
CompletionValueKind = Literal["none", "file", "dir"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def complete_with(action: argparse.Action, value_kind: CompletionValueKind) -> argparse.Action:
|
|
19
|
+
"""Annotate an argparse action with a shell completion value kind."""
|
|
20
|
+
action.__dict__["_ixt_completion_value_kind"] = value_kind
|
|
21
|
+
return action
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class CompletionArgument:
|
|
26
|
+
"""One positional argument exposed by an argparse command."""
|
|
27
|
+
|
|
28
|
+
name: str
|
|
29
|
+
help: str = ""
|
|
30
|
+
choices: tuple[str, ...] = ()
|
|
31
|
+
value_kind: CompletionValueKind = "none"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class CompletionOption:
|
|
36
|
+
"""One option exposed by an argparse command."""
|
|
37
|
+
|
|
38
|
+
flags: tuple[str, ...]
|
|
39
|
+
help: str = ""
|
|
40
|
+
choices: tuple[str, ...] = ()
|
|
41
|
+
takes_value: bool = False
|
|
42
|
+
value_kind: CompletionValueKind = "none"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class CompletionCommand:
|
|
47
|
+
"""A command node extracted from argparse."""
|
|
48
|
+
|
|
49
|
+
path: tuple[str, ...] = ()
|
|
50
|
+
help: str = ""
|
|
51
|
+
options: tuple[CompletionOption, ...] = ()
|
|
52
|
+
arguments: tuple[CompletionArgument, ...] = ()
|
|
53
|
+
subcommands: tuple[CompletionCommand, ...] = field(default_factory=tuple)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class CompletionCandidate:
|
|
58
|
+
"""One word that can be completed, optionally with a description."""
|
|
59
|
+
|
|
60
|
+
word: str
|
|
61
|
+
help: str = ""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def render_completion(parser: argparse.ArgumentParser, shell: str) -> str:
|
|
65
|
+
"""Render a completion script for *shell* from *parser*."""
|
|
66
|
+
tree = extract_completion_tree(parser)
|
|
67
|
+
if shell == "bash":
|
|
68
|
+
return _render_bash(tree)
|
|
69
|
+
if shell == "zsh":
|
|
70
|
+
return _render_zsh(tree)
|
|
71
|
+
if shell == "fish":
|
|
72
|
+
return _render_fish(tree)
|
|
73
|
+
supported = ", ".join(SUPPORTED_COMPLETION_SHELLS)
|
|
74
|
+
raise ValueError(f"unsupported completion shell: {shell} (expected one of: {supported})")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def extract_completion_tree(parser: argparse.ArgumentParser) -> CompletionCommand:
|
|
78
|
+
"""Extract command, option, and choice metadata from *parser*."""
|
|
79
|
+
return _extract_command(parser, path=(), help_text=parser.description or "")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _extract_command(
|
|
83
|
+
parser: argparse.ArgumentParser,
|
|
84
|
+
*,
|
|
85
|
+
path: tuple[str, ...],
|
|
86
|
+
help_text: str = "",
|
|
87
|
+
) -> CompletionCommand:
|
|
88
|
+
options: list[CompletionOption] = []
|
|
89
|
+
arguments: list[CompletionArgument] = []
|
|
90
|
+
subcommands: list[CompletionCommand] = []
|
|
91
|
+
|
|
92
|
+
for action in parser._actions:
|
|
93
|
+
if _is_subparsers_action(action):
|
|
94
|
+
helps = {
|
|
95
|
+
choice.dest: choice.help or "" for choice in getattr(action, "_choices_actions", [])
|
|
96
|
+
}
|
|
97
|
+
hidden = {
|
|
98
|
+
str(choice.dest)
|
|
99
|
+
for choice in getattr(action, "_choices_actions", [])
|
|
100
|
+
if choice.help == argparse.SUPPRESS
|
|
101
|
+
}
|
|
102
|
+
choices = getattr(action, "choices", None)
|
|
103
|
+
if not isinstance(choices, dict):
|
|
104
|
+
continue
|
|
105
|
+
for name, subparser in choices.items():
|
|
106
|
+
if not isinstance(subparser, argparse.ArgumentParser):
|
|
107
|
+
continue
|
|
108
|
+
if str(name) in hidden:
|
|
109
|
+
continue
|
|
110
|
+
subcommands.append(
|
|
111
|
+
_extract_command(
|
|
112
|
+
subparser,
|
|
113
|
+
path=(*path, str(name)),
|
|
114
|
+
help_text=helps.get(str(name), subparser.description or ""),
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
if action.help == argparse.SUPPRESS:
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
if action.option_strings:
|
|
123
|
+
options.append(
|
|
124
|
+
CompletionOption(
|
|
125
|
+
flags=tuple(action.option_strings),
|
|
126
|
+
help=action.help or "",
|
|
127
|
+
choices=_choices(action),
|
|
128
|
+
takes_value=getattr(action, "nargs", None) != 0,
|
|
129
|
+
value_kind=_value_kind(action),
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
else:
|
|
133
|
+
arguments.append(
|
|
134
|
+
CompletionArgument(
|
|
135
|
+
name=action.dest,
|
|
136
|
+
help=action.help or "",
|
|
137
|
+
choices=_choices(action),
|
|
138
|
+
value_kind=_value_kind(action),
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return CompletionCommand(
|
|
143
|
+
path=path,
|
|
144
|
+
help=help_text,
|
|
145
|
+
options=tuple(options),
|
|
146
|
+
arguments=tuple(arguments),
|
|
147
|
+
subcommands=tuple(subcommands),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _is_subparsers_action(action: argparse.Action) -> bool:
|
|
152
|
+
return action.__class__.__name__ == "_SubParsersAction"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _choices(action: argparse.Action) -> tuple[str, ...]:
|
|
156
|
+
choices = getattr(action, "choices", None)
|
|
157
|
+
if choices is None or isinstance(choices, dict):
|
|
158
|
+
return ()
|
|
159
|
+
return tuple(str(choice) for choice in choices)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _value_kind(action: argparse.Action) -> CompletionValueKind:
|
|
163
|
+
value = getattr(action, "_ixt_completion_value_kind", "none")
|
|
164
|
+
if value in ("none", "file", "dir"):
|
|
165
|
+
return value
|
|
166
|
+
return "none"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _iter_commands(command: CompletionCommand) -> list[CompletionCommand]:
|
|
170
|
+
commands = [command]
|
|
171
|
+
for child in command.subcommands:
|
|
172
|
+
commands.extend(_iter_commands(child))
|
|
173
|
+
return commands
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _path_key(path: tuple[str, ...]) -> str:
|
|
177
|
+
return " ".join(path)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _command_candidates(command: CompletionCommand) -> list[str]:
|
|
181
|
+
return [candidate.word for candidate in _command_candidate_details(command)]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _command_candidate_details(command: CompletionCommand) -> list[CompletionCandidate]:
|
|
185
|
+
words: list[CompletionCandidate] = []
|
|
186
|
+
words.extend(CompletionCandidate(child.path[-1], child.help) for child in command.subcommands)
|
|
187
|
+
for option in command.options:
|
|
188
|
+
words.extend(CompletionCandidate(flag, option.help) for flag in option.flags)
|
|
189
|
+
return words
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _argument_value_completion(command: CompletionCommand) -> CompletionArgument | None:
|
|
193
|
+
if command.subcommands:
|
|
194
|
+
return None
|
|
195
|
+
for argument in command.arguments:
|
|
196
|
+
if argument.choices or argument.value_kind != "none":
|
|
197
|
+
return argument
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _word_list(words: list[str] | tuple[str, ...]) -> str:
|
|
202
|
+
return " ".join(words)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _zsh_quote(value: str) -> str:
|
|
206
|
+
return (
|
|
207
|
+
'"'
|
|
208
|
+
+ (value.replace("\\", "\\\\").replace('"', '\\"').replace("$", "\\$").replace("`", "\\`"))
|
|
209
|
+
+ '"'
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _zsh_candidate(value: str, help_text: str = "") -> str:
|
|
214
|
+
return _zsh_quote(f"{value}:{help_text}" if help_text else value)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _case_label(value: str) -> str:
|
|
218
|
+
return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _bash_value_completion_lines(
|
|
222
|
+
*,
|
|
223
|
+
value_kind: CompletionValueKind,
|
|
224
|
+
choices: tuple[str, ...] = (),
|
|
225
|
+
) -> list[str]:
|
|
226
|
+
if choices:
|
|
227
|
+
return [
|
|
228
|
+
f' COMPREPLY=( $(compgen -W "{_word_list(choices)}" -- "$cur") )',
|
|
229
|
+
" return 0",
|
|
230
|
+
]
|
|
231
|
+
if value_kind == "file":
|
|
232
|
+
return [
|
|
233
|
+
' COMPREPLY=( $(compgen -f -- "$cur") )',
|
|
234
|
+
" return 0",
|
|
235
|
+
]
|
|
236
|
+
if value_kind == "dir":
|
|
237
|
+
return [
|
|
238
|
+
' COMPREPLY=( $(compgen -d -- "$cur") )',
|
|
239
|
+
" return 0",
|
|
240
|
+
]
|
|
241
|
+
return [" return 0"]
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _zsh_value_completion_lines(
|
|
245
|
+
*,
|
|
246
|
+
value_kind: CompletionValueKind,
|
|
247
|
+
choices: tuple[str, ...] = (),
|
|
248
|
+
) -> list[str]:
|
|
249
|
+
if choices:
|
|
250
|
+
return [
|
|
251
|
+
f" compadd -- {_word_list(choices)}",
|
|
252
|
+
" return",
|
|
253
|
+
]
|
|
254
|
+
if value_kind == "file":
|
|
255
|
+
return [
|
|
256
|
+
" _files",
|
|
257
|
+
" return",
|
|
258
|
+
]
|
|
259
|
+
if value_kind == "dir":
|
|
260
|
+
return [
|
|
261
|
+
" _files -/",
|
|
262
|
+
" return",
|
|
263
|
+
]
|
|
264
|
+
return [" return"]
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _bash_path_detector(root: CompletionCommand) -> list[str]:
|
|
268
|
+
lines = [
|
|
269
|
+
' local path=""',
|
|
270
|
+
" local word",
|
|
271
|
+
" local idx=1",
|
|
272
|
+
" while [[ $idx -lt $COMP_CWORD ]]; do",
|
|
273
|
+
' word="${COMP_WORDS[$idx]}"',
|
|
274
|
+
' case "$path" in',
|
|
275
|
+
]
|
|
276
|
+
for command in _iter_commands(root):
|
|
277
|
+
if not command.subcommands:
|
|
278
|
+
continue
|
|
279
|
+
key = _path_key(command.path)
|
|
280
|
+
patterns = "|".join(child.path[-1] for child in command.subcommands)
|
|
281
|
+
next_path = "$word" if not key else f"{key} $word"
|
|
282
|
+
lines.extend(
|
|
283
|
+
[
|
|
284
|
+
f" {_case_label(key)})",
|
|
285
|
+
' case "$word" in',
|
|
286
|
+
f' {patterns}) path="{next_path}" ;;',
|
|
287
|
+
" esac",
|
|
288
|
+
" ;;",
|
|
289
|
+
]
|
|
290
|
+
)
|
|
291
|
+
lines.extend(
|
|
292
|
+
[
|
|
293
|
+
" esac",
|
|
294
|
+
" ((idx++))",
|
|
295
|
+
" done",
|
|
296
|
+
]
|
|
297
|
+
)
|
|
298
|
+
return lines
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _zsh_path_detector(root: CompletionCommand) -> list[str]:
|
|
302
|
+
lines = [
|
|
303
|
+
' local path=""',
|
|
304
|
+
" local word",
|
|
305
|
+
" local idx=2",
|
|
306
|
+
" while (( idx < CURRENT )); do",
|
|
307
|
+
' word="${words[idx]}"',
|
|
308
|
+
' case "$path" in',
|
|
309
|
+
]
|
|
310
|
+
for command in _iter_commands(root):
|
|
311
|
+
if not command.subcommands:
|
|
312
|
+
continue
|
|
313
|
+
key = _path_key(command.path)
|
|
314
|
+
patterns = "|".join(child.path[-1] for child in command.subcommands)
|
|
315
|
+
next_path = "$word" if not key else f"{key} $word"
|
|
316
|
+
lines.extend(
|
|
317
|
+
[
|
|
318
|
+
f" {_case_label(key)})",
|
|
319
|
+
' case "$word" in',
|
|
320
|
+
f' {patterns}) path="{next_path}" ;;',
|
|
321
|
+
" esac",
|
|
322
|
+
" ;;",
|
|
323
|
+
]
|
|
324
|
+
)
|
|
325
|
+
lines.extend(
|
|
326
|
+
[
|
|
327
|
+
" esac",
|
|
328
|
+
" (( idx++ ))",
|
|
329
|
+
" done",
|
|
330
|
+
]
|
|
331
|
+
)
|
|
332
|
+
return lines
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _render_bash(root: CompletionCommand) -> str:
|
|
336
|
+
lines = [
|
|
337
|
+
"# bash completion for ixt",
|
|
338
|
+
"_ixt_completion() {",
|
|
339
|
+
" local cur prev",
|
|
340
|
+
" COMPREPLY=()",
|
|
341
|
+
' cur="${COMP_WORDS[COMP_CWORD]}"',
|
|
342
|
+
' prev="${COMP_WORDS[COMP_CWORD-1]}"',
|
|
343
|
+
"",
|
|
344
|
+
]
|
|
345
|
+
lines.extend(_bash_path_detector(root))
|
|
346
|
+
lines.extend(["", ' case "$path|$prev" in'])
|
|
347
|
+
for command in _iter_commands(root):
|
|
348
|
+
key = _path_key(command.path)
|
|
349
|
+
for option in command.options:
|
|
350
|
+
if not option.takes_value:
|
|
351
|
+
continue
|
|
352
|
+
for flag in option.flags:
|
|
353
|
+
lines.extend(
|
|
354
|
+
[
|
|
355
|
+
f" {_case_label(f'{key}|{flag}')})",
|
|
356
|
+
*_bash_value_completion_lines(
|
|
357
|
+
choices=option.choices,
|
|
358
|
+
value_kind=option.value_kind,
|
|
359
|
+
),
|
|
360
|
+
" ;;",
|
|
361
|
+
]
|
|
362
|
+
)
|
|
363
|
+
lines.extend([" esac", "", ' case "$path" in'])
|
|
364
|
+
for command in _iter_commands(root):
|
|
365
|
+
candidates = _command_candidates(command)
|
|
366
|
+
argument = _argument_value_completion(command)
|
|
367
|
+
if not candidates and argument is None:
|
|
368
|
+
continue
|
|
369
|
+
key = _path_key(command.path)
|
|
370
|
+
words = _word_list(candidates)
|
|
371
|
+
lines.append(f" {_case_label(key)})")
|
|
372
|
+
if words:
|
|
373
|
+
lines.append(f' COMPREPLY=( $(compgen -W "{words}" -- "$cur") )')
|
|
374
|
+
if argument is not None:
|
|
375
|
+
if argument.choices:
|
|
376
|
+
lines.append(
|
|
377
|
+
f' COMPREPLY+=( $(compgen -W "{_word_list(argument.choices)}" -- "$cur") )'
|
|
378
|
+
)
|
|
379
|
+
elif argument.value_kind == "file":
|
|
380
|
+
lines.append(' COMPREPLY+=( $(compgen -f -- "$cur") )')
|
|
381
|
+
elif argument.value_kind == "dir":
|
|
382
|
+
lines.append(' COMPREPLY+=( $(compgen -d -- "$cur") )')
|
|
383
|
+
lines.append(" ;;")
|
|
384
|
+
lines.extend(
|
|
385
|
+
[
|
|
386
|
+
" esac",
|
|
387
|
+
" return 0",
|
|
388
|
+
"}",
|
|
389
|
+
"complete -F _ixt_completion ixt",
|
|
390
|
+
"",
|
|
391
|
+
]
|
|
392
|
+
)
|
|
393
|
+
return "\n".join(lines)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _render_zsh(root: CompletionCommand) -> str:
|
|
397
|
+
lines = [
|
|
398
|
+
"#compdef ixt",
|
|
399
|
+
"",
|
|
400
|
+
"_ixt() {",
|
|
401
|
+
" local cur prev",
|
|
402
|
+
' cur="${words[CURRENT]}"',
|
|
403
|
+
' prev="${words[CURRENT-1]}"',
|
|
404
|
+
"",
|
|
405
|
+
]
|
|
406
|
+
lines.extend(_zsh_path_detector(root))
|
|
407
|
+
lines.extend(["", ' case "$path|$prev" in'])
|
|
408
|
+
for command in _iter_commands(root):
|
|
409
|
+
key = _path_key(command.path)
|
|
410
|
+
for option in command.options:
|
|
411
|
+
if not option.takes_value:
|
|
412
|
+
continue
|
|
413
|
+
for flag in option.flags:
|
|
414
|
+
lines.extend(
|
|
415
|
+
[
|
|
416
|
+
f" {_case_label(f'{key}|{flag}')})",
|
|
417
|
+
*_zsh_value_completion_lines(
|
|
418
|
+
choices=option.choices,
|
|
419
|
+
value_kind=option.value_kind,
|
|
420
|
+
),
|
|
421
|
+
" ;;",
|
|
422
|
+
]
|
|
423
|
+
)
|
|
424
|
+
lines.extend([" esac", "", ' case "$path" in'])
|
|
425
|
+
for command in _iter_commands(root):
|
|
426
|
+
candidates = _command_candidate_details(command)
|
|
427
|
+
argument = _argument_value_completion(command)
|
|
428
|
+
if not candidates and argument is None:
|
|
429
|
+
continue
|
|
430
|
+
key = _path_key(command.path)
|
|
431
|
+
lines.append(f" {_case_label(key)})")
|
|
432
|
+
if candidates:
|
|
433
|
+
lines.append(" local -a candidates")
|
|
434
|
+
lines.append(" candidates=(")
|
|
435
|
+
for candidate in candidates:
|
|
436
|
+
lines.append(f" {_zsh_candidate(candidate.word, candidate.help)}")
|
|
437
|
+
lines.append(" )")
|
|
438
|
+
lines.append(" _describe 'ixt completions' candidates")
|
|
439
|
+
if argument is not None:
|
|
440
|
+
lines.extend(
|
|
441
|
+
" " + line
|
|
442
|
+
for line in _zsh_value_completion_lines(
|
|
443
|
+
choices=argument.choices,
|
|
444
|
+
value_kind=argument.value_kind,
|
|
445
|
+
)
|
|
446
|
+
if "return" not in line
|
|
447
|
+
)
|
|
448
|
+
lines.append(" ;;")
|
|
449
|
+
lines.extend(
|
|
450
|
+
[
|
|
451
|
+
" esac",
|
|
452
|
+
"}",
|
|
453
|
+
"",
|
|
454
|
+
"compdef _ixt ixt",
|
|
455
|
+
"",
|
|
456
|
+
]
|
|
457
|
+
)
|
|
458
|
+
return "\n".join(lines)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _fish_quote(value: str) -> str:
|
|
462
|
+
return "'" + value.replace("\\", "\\\\").replace("'", "\\'") + "'"
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _fish_condition_for_path(path: tuple[str, ...]) -> str:
|
|
466
|
+
if not path:
|
|
467
|
+
return ""
|
|
468
|
+
return "; and ".join(f"__fish_seen_subcommand_from {part}" for part in path)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _fish_condition_for_child(parent: CompletionCommand) -> str:
|
|
472
|
+
if not parent.path:
|
|
473
|
+
return "__fish_use_subcommand"
|
|
474
|
+
pieces = [_fish_condition_for_path(parent.path)]
|
|
475
|
+
if parent.subcommands:
|
|
476
|
+
siblings = " ".join(child.path[-1] for child in parent.subcommands)
|
|
477
|
+
pieces.append(f"not __fish_seen_subcommand_from {siblings}")
|
|
478
|
+
return "; and ".join(piece for piece in pieces if piece)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _fish_option_names(option: CompletionOption) -> list[str]:
|
|
482
|
+
parts: list[str] = []
|
|
483
|
+
long_flags = [flag for flag in option.flags if flag.startswith("--")]
|
|
484
|
+
short_flags = [
|
|
485
|
+
flag for flag in option.flags if flag.startswith("-") and not flag.startswith("--")
|
|
486
|
+
]
|
|
487
|
+
if long_flags:
|
|
488
|
+
parts.extend(["-l", long_flags[0][2:]])
|
|
489
|
+
if short_flags:
|
|
490
|
+
parts.extend(["-s", short_flags[0][1:]])
|
|
491
|
+
return parts
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _fish_argument_value(argument: CompletionArgument) -> str | None:
|
|
495
|
+
if argument.choices:
|
|
496
|
+
return " ".join(argument.choices)
|
|
497
|
+
if argument.value_kind == "dir":
|
|
498
|
+
return "(__fish_complete_directories)"
|
|
499
|
+
if argument.value_kind == "file":
|
|
500
|
+
return "(__fish_complete_path)"
|
|
501
|
+
return None
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _render_fish(root: CompletionCommand) -> str:
|
|
505
|
+
lines = ["# fish completion for ixt"]
|
|
506
|
+
for command in _iter_commands(root):
|
|
507
|
+
condition = _fish_condition_for_path(command.path)
|
|
508
|
+
for option in command.options:
|
|
509
|
+
parts = ["complete", "-c", "ixt"]
|
|
510
|
+
if option.value_kind not in ("file", "dir"):
|
|
511
|
+
parts.append("-f")
|
|
512
|
+
if condition:
|
|
513
|
+
parts.extend(["-n", _fish_quote(condition)])
|
|
514
|
+
parts.extend(_fish_option_names(option))
|
|
515
|
+
if option.takes_value:
|
|
516
|
+
parts.append("-r")
|
|
517
|
+
if option.choices:
|
|
518
|
+
parts.extend(["-a", _fish_quote(" ".join(option.choices))])
|
|
519
|
+
elif option.value_kind == "dir":
|
|
520
|
+
parts.extend(["-a", _fish_quote("(__fish_complete_directories)")])
|
|
521
|
+
elif option.value_kind == "file":
|
|
522
|
+
parts.extend(["-a", _fish_quote("(__fish_complete_path)")])
|
|
523
|
+
if option.help:
|
|
524
|
+
parts.extend(["-d", _fish_quote(option.help)])
|
|
525
|
+
lines.append(" ".join(parts))
|
|
526
|
+
|
|
527
|
+
argument = _argument_value_completion(command)
|
|
528
|
+
argument_value = _fish_argument_value(argument) if argument else None
|
|
529
|
+
if argument is not None and argument_value is not None:
|
|
530
|
+
parts = ["complete", "-c", "ixt"]
|
|
531
|
+
if condition:
|
|
532
|
+
parts.extend(["-n", _fish_quote(condition)])
|
|
533
|
+
if argument.value_kind == "dir":
|
|
534
|
+
parts.append("-f")
|
|
535
|
+
parts.extend(["-a", _fish_quote(argument_value)])
|
|
536
|
+
if argument.help:
|
|
537
|
+
parts.extend(["-d", _fish_quote(argument.help)])
|
|
538
|
+
lines.append(" ".join(parts))
|
|
539
|
+
|
|
540
|
+
child_condition = _fish_condition_for_child(command)
|
|
541
|
+
for child in command.subcommands:
|
|
542
|
+
parts = [
|
|
543
|
+
"complete",
|
|
544
|
+
"-c",
|
|
545
|
+
"ixt",
|
|
546
|
+
"-f",
|
|
547
|
+
"-n",
|
|
548
|
+
_fish_quote(child_condition),
|
|
549
|
+
"-a",
|
|
550
|
+
_fish_quote(child.path[-1]),
|
|
551
|
+
]
|
|
552
|
+
if child.help:
|
|
553
|
+
parts.extend(["-d", _fish_quote(child.help)])
|
|
554
|
+
lines.append(" ".join(parts))
|
|
555
|
+
|
|
556
|
+
lines.append("")
|
|
557
|
+
return "\n".join(lines)
|