yuho 5.0.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.
- yuho/__init__.py +16 -0
- yuho/ast/__init__.py +196 -0
- yuho/ast/builder.py +926 -0
- yuho/ast/constant_folder.py +280 -0
- yuho/ast/dead_code.py +199 -0
- yuho/ast/exhaustiveness.py +503 -0
- yuho/ast/nodes.py +907 -0
- yuho/ast/overlap.py +291 -0
- yuho/ast/reachability.py +293 -0
- yuho/ast/scope_analysis.py +490 -0
- yuho/ast/transformer.py +490 -0
- yuho/ast/type_check.py +471 -0
- yuho/ast/type_inference.py +425 -0
- yuho/ast/visitor.py +239 -0
- yuho/cli/__init__.py +14 -0
- yuho/cli/commands/__init__.py +1 -0
- yuho/cli/commands/api.py +431 -0
- yuho/cli/commands/ast_viz.py +334 -0
- yuho/cli/commands/check.py +218 -0
- yuho/cli/commands/config.py +311 -0
- yuho/cli/commands/contribute.py +122 -0
- yuho/cli/commands/diff.py +487 -0
- yuho/cli/commands/explain.py +240 -0
- yuho/cli/commands/fmt.py +253 -0
- yuho/cli/commands/generate.py +316 -0
- yuho/cli/commands/graph.py +410 -0
- yuho/cli/commands/init.py +120 -0
- yuho/cli/commands/library.py +656 -0
- yuho/cli/commands/lint.py +503 -0
- yuho/cli/commands/lsp.py +36 -0
- yuho/cli/commands/preview.py +377 -0
- yuho/cli/commands/repl.py +444 -0
- yuho/cli/commands/serve.py +44 -0
- yuho/cli/commands/test.py +528 -0
- yuho/cli/commands/transpile.py +121 -0
- yuho/cli/commands/wizard.py +370 -0
- yuho/cli/completions.py +182 -0
- yuho/cli/error_formatter.py +193 -0
- yuho/cli/main.py +1064 -0
- yuho/config/__init__.py +46 -0
- yuho/config/loader.py +235 -0
- yuho/config/mask.py +194 -0
- yuho/config/schema.py +147 -0
- yuho/library/__init__.py +84 -0
- yuho/library/index.py +328 -0
- yuho/library/install.py +699 -0
- yuho/library/lockfile.py +330 -0
- yuho/library/package.py +421 -0
- yuho/library/resolver.py +791 -0
- yuho/library/signature.py +335 -0
- yuho/llm/__init__.py +45 -0
- yuho/llm/config.py +75 -0
- yuho/llm/factory.py +123 -0
- yuho/llm/prompts.py +146 -0
- yuho/llm/providers.py +383 -0
- yuho/llm/utils.py +470 -0
- yuho/lsp/__init__.py +14 -0
- yuho/lsp/code_action_handler.py +518 -0
- yuho/lsp/completion_handler.py +85 -0
- yuho/lsp/diagnostics.py +100 -0
- yuho/lsp/hover_handler.py +130 -0
- yuho/lsp/server.py +1425 -0
- yuho/mcp/__init__.py +10 -0
- yuho/mcp/server.py +1452 -0
- yuho/parser/__init__.py +8 -0
- yuho/parser/source_location.py +108 -0
- yuho/parser/wrapper.py +311 -0
- yuho/testing/__init__.py +48 -0
- yuho/testing/coverage.py +274 -0
- yuho/testing/fixtures.py +263 -0
- yuho/transpile/__init__.py +52 -0
- yuho/transpile/alloy_transpiler.py +546 -0
- yuho/transpile/base.py +100 -0
- yuho/transpile/blocks_transpiler.py +338 -0
- yuho/transpile/english_transpiler.py +470 -0
- yuho/transpile/graphql_transpiler.py +404 -0
- yuho/transpile/json_transpiler.py +217 -0
- yuho/transpile/jsonld_transpiler.py +250 -0
- yuho/transpile/latex_preamble.py +161 -0
- yuho/transpile/latex_transpiler.py +406 -0
- yuho/transpile/latex_utils.py +206 -0
- yuho/transpile/mermaid_transpiler.py +357 -0
- yuho/transpile/registry.py +275 -0
- yuho/verify/__init__.py +43 -0
- yuho/verify/alloy.py +352 -0
- yuho/verify/combined.py +218 -0
- yuho/verify/z3_solver.py +1155 -0
- yuho-5.0.0.dist-info/METADATA +186 -0
- yuho-5.0.0.dist-info/RECORD +91 -0
- yuho-5.0.0.dist-info/WHEEL +4 -0
- yuho-5.0.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TUI wizard for interactive statute creation.
|
|
3
|
+
|
|
4
|
+
Provides a step-by-step wizard interface for building Yuho statutes
|
|
5
|
+
using inquirer-style prompts. Generates valid .yh files from user input.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional, List, Dict, Any
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ElementData:
|
|
18
|
+
"""Data for a statute element."""
|
|
19
|
+
element_type: str # "actus_reus", "mens_rea", "circumstance"
|
|
20
|
+
name: str
|
|
21
|
+
description: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class DefinitionData:
|
|
26
|
+
"""Data for a definition entry."""
|
|
27
|
+
term: str
|
|
28
|
+
definition: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class IllustrationData:
|
|
33
|
+
"""Data for an illustration."""
|
|
34
|
+
label: str
|
|
35
|
+
description: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class PenaltyData:
|
|
40
|
+
"""Data for penalty specification."""
|
|
41
|
+
has_imprisonment: bool = False
|
|
42
|
+
imprisonment_min: str = ""
|
|
43
|
+
imprisonment_max: str = ""
|
|
44
|
+
has_fine: bool = False
|
|
45
|
+
fine_min: str = ""
|
|
46
|
+
fine_max: str = ""
|
|
47
|
+
supplementary: str = ""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class StatuteData:
|
|
52
|
+
"""Complete data for a statute."""
|
|
53
|
+
section_number: str = ""
|
|
54
|
+
title: str = ""
|
|
55
|
+
definitions: List[DefinitionData] = field(default_factory=list)
|
|
56
|
+
elements: List[ElementData] = field(default_factory=list)
|
|
57
|
+
penalty: Optional[PenaltyData] = None
|
|
58
|
+
illustrations: List[IllustrationData] = field(default_factory=list)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def prompt_input(prompt: str, default: str = "", required: bool = False) -> str:
|
|
62
|
+
"""Prompt for text input."""
|
|
63
|
+
while True:
|
|
64
|
+
if default:
|
|
65
|
+
result = click.prompt(prompt, default=default, show_default=True)
|
|
66
|
+
else:
|
|
67
|
+
result = click.prompt(prompt, default="", show_default=False)
|
|
68
|
+
|
|
69
|
+
if required and not result.strip():
|
|
70
|
+
click.echo(" This field is required.")
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
return result.strip()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def prompt_confirm(prompt: str, default: bool = False) -> bool:
|
|
77
|
+
"""Prompt for yes/no confirmation."""
|
|
78
|
+
return click.confirm(prompt, default=default)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def prompt_choice(prompt: str, choices: List[str], default: int = 0) -> str:
|
|
82
|
+
"""Prompt for choice from list."""
|
|
83
|
+
click.echo(f"\n{prompt}")
|
|
84
|
+
for i, choice in enumerate(choices):
|
|
85
|
+
marker = ">" if i == default else " "
|
|
86
|
+
click.echo(f" {marker} [{i + 1}] {choice}")
|
|
87
|
+
|
|
88
|
+
while True:
|
|
89
|
+
value = click.prompt("Enter number", default=str(default + 1))
|
|
90
|
+
try:
|
|
91
|
+
idx = int(value) - 1
|
|
92
|
+
if 0 <= idx < len(choices):
|
|
93
|
+
return choices[idx]
|
|
94
|
+
except ValueError:
|
|
95
|
+
pass
|
|
96
|
+
click.echo(f" Please enter a number between 1 and {len(choices)}")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def wizard_header(title: str) -> None:
|
|
100
|
+
"""Print a wizard section header."""
|
|
101
|
+
click.echo("")
|
|
102
|
+
click.echo(click.style(f"═══ {title} ═══", fg="cyan", bold=True))
|
|
103
|
+
click.echo("")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def collect_definitions(data: StatuteData) -> None:
|
|
107
|
+
"""Collect definition entries."""
|
|
108
|
+
wizard_header("Definitions")
|
|
109
|
+
click.echo("Define key terms used in the statute.")
|
|
110
|
+
click.echo("(Press Enter with empty term to finish)")
|
|
111
|
+
click.echo("")
|
|
112
|
+
|
|
113
|
+
while True:
|
|
114
|
+
term = prompt_input("Term to define")
|
|
115
|
+
if not term:
|
|
116
|
+
break
|
|
117
|
+
|
|
118
|
+
definition = prompt_input(f" Definition of '{term}'", required=True)
|
|
119
|
+
data.definitions.append(DefinitionData(term=term, definition=definition))
|
|
120
|
+
click.echo(click.style(f" ✓ Added definition: {term}", fg="green"))
|
|
121
|
+
|
|
122
|
+
click.echo(f"\nTotal definitions: {len(data.definitions)}")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def collect_elements(data: StatuteData) -> None:
|
|
126
|
+
"""Collect offense elements."""
|
|
127
|
+
wizard_header("Elements of the Offense")
|
|
128
|
+
click.echo("Define the elements (actus reus, mens rea, circumstances)")
|
|
129
|
+
click.echo("")
|
|
130
|
+
|
|
131
|
+
# Actus Reus
|
|
132
|
+
if prompt_confirm("Add actus reus (physical act) element?", default=True):
|
|
133
|
+
name = prompt_input(" Element name/identifier", default="act", required=True)
|
|
134
|
+
desc = prompt_input(" Description of the act", required=True)
|
|
135
|
+
data.elements.append(ElementData(
|
|
136
|
+
element_type="actus_reus",
|
|
137
|
+
name=name,
|
|
138
|
+
description=desc
|
|
139
|
+
))
|
|
140
|
+
click.echo(click.style(f" ✓ Added actus_reus: {name}", fg="green"))
|
|
141
|
+
|
|
142
|
+
# Mens Rea
|
|
143
|
+
if prompt_confirm("Add mens rea (mental state) element?", default=True):
|
|
144
|
+
name = prompt_input(" Element name/identifier", default="intent", required=True)
|
|
145
|
+
desc = prompt_input(" Description of required mental state", required=True)
|
|
146
|
+
data.elements.append(ElementData(
|
|
147
|
+
element_type="mens_rea",
|
|
148
|
+
name=name,
|
|
149
|
+
description=desc
|
|
150
|
+
))
|
|
151
|
+
click.echo(click.style(f" ✓ Added mens_rea: {name}", fg="green"))
|
|
152
|
+
|
|
153
|
+
# Additional elements
|
|
154
|
+
while prompt_confirm("Add another element?", default=False):
|
|
155
|
+
element_type = prompt_choice("Element type:", [
|
|
156
|
+
"actus_reus",
|
|
157
|
+
"mens_rea",
|
|
158
|
+
"circumstance"
|
|
159
|
+
])
|
|
160
|
+
name = prompt_input(" Element name", required=True)
|
|
161
|
+
desc = prompt_input(" Description", required=True)
|
|
162
|
+
data.elements.append(ElementData(
|
|
163
|
+
element_type=element_type,
|
|
164
|
+
name=name,
|
|
165
|
+
description=desc
|
|
166
|
+
))
|
|
167
|
+
click.echo(click.style(f" ✓ Added {element_type}: {name}", fg="green"))
|
|
168
|
+
|
|
169
|
+
click.echo(f"\nTotal elements: {len(data.elements)}")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def collect_penalty(data: StatuteData) -> None:
|
|
173
|
+
"""Collect penalty specification."""
|
|
174
|
+
wizard_header("Penalty")
|
|
175
|
+
|
|
176
|
+
if not prompt_confirm("Does this statute have a penalty clause?", default=True):
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
penalty = PenaltyData()
|
|
180
|
+
|
|
181
|
+
# Imprisonment
|
|
182
|
+
if prompt_confirm("Include imprisonment?", default=True):
|
|
183
|
+
penalty.has_imprisonment = True
|
|
184
|
+
penalty.imprisonment_max = prompt_input(
|
|
185
|
+
" Maximum imprisonment (e.g., '10 years', '3Y')",
|
|
186
|
+
required=True
|
|
187
|
+
)
|
|
188
|
+
if prompt_confirm(" Specify minimum imprisonment?", default=False):
|
|
189
|
+
penalty.imprisonment_min = prompt_input(" Minimum imprisonment")
|
|
190
|
+
|
|
191
|
+
# Fine
|
|
192
|
+
if prompt_confirm("Include fine?", default=True):
|
|
193
|
+
penalty.has_fine = True
|
|
194
|
+
penalty.fine_max = prompt_input(
|
|
195
|
+
" Maximum fine (e.g., 'SGD 10000', '$5000')",
|
|
196
|
+
required=True
|
|
197
|
+
)
|
|
198
|
+
if prompt_confirm(" Specify minimum fine?", default=False):
|
|
199
|
+
penalty.fine_min = prompt_input(" Minimum fine")
|
|
200
|
+
|
|
201
|
+
# Supplementary
|
|
202
|
+
if prompt_confirm("Add supplementary penalty info?", default=False):
|
|
203
|
+
penalty.supplementary = prompt_input(" Supplementary information")
|
|
204
|
+
|
|
205
|
+
data.penalty = penalty
|
|
206
|
+
click.echo(click.style(" ✓ Penalty configured", fg="green"))
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def collect_illustrations(data: StatuteData) -> None:
|
|
210
|
+
"""Collect illustration examples."""
|
|
211
|
+
wizard_header("Illustrations")
|
|
212
|
+
click.echo("Add examples to illustrate the statute's application.")
|
|
213
|
+
click.echo("(Press Enter with empty label to finish)")
|
|
214
|
+
click.echo("")
|
|
215
|
+
|
|
216
|
+
label_counter = ord('a')
|
|
217
|
+
|
|
218
|
+
while True:
|
|
219
|
+
default_label = f"({chr(label_counter)})"
|
|
220
|
+
label = prompt_input(f"Illustration label", default=default_label)
|
|
221
|
+
|
|
222
|
+
if not label or label == default_label:
|
|
223
|
+
# Check if user wants to add with default label
|
|
224
|
+
if not label:
|
|
225
|
+
break
|
|
226
|
+
|
|
227
|
+
desc = prompt_input(" Illustration text", required=True)
|
|
228
|
+
data.illustrations.append(IllustrationData(label=label, description=desc))
|
|
229
|
+
click.echo(click.style(f" ✓ Added illustration {label}", fg="green"))
|
|
230
|
+
label_counter += 1
|
|
231
|
+
|
|
232
|
+
if not prompt_confirm("Add another illustration?", default=False):
|
|
233
|
+
break
|
|
234
|
+
|
|
235
|
+
click.echo(f"\nTotal illustrations: {len(data.illustrations)}")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def generate_yuho_code(data: StatuteData) -> str:
|
|
239
|
+
"""Generate Yuho source code from collected data."""
|
|
240
|
+
lines: List[str] = []
|
|
241
|
+
|
|
242
|
+
# Statute header
|
|
243
|
+
lines.append(f'statute "{data.section_number}" "{data.title}" {{')
|
|
244
|
+
|
|
245
|
+
# Definitions
|
|
246
|
+
if data.definitions:
|
|
247
|
+
lines.append("")
|
|
248
|
+
lines.append(" definitions {")
|
|
249
|
+
for defn in data.definitions:
|
|
250
|
+
escaped_def = defn.definition.replace('"', '\\"')
|
|
251
|
+
lines.append(f' "{defn.term}" := "{escaped_def}";')
|
|
252
|
+
lines.append(" }")
|
|
253
|
+
|
|
254
|
+
# Elements
|
|
255
|
+
if data.elements:
|
|
256
|
+
lines.append("")
|
|
257
|
+
lines.append(" elements {")
|
|
258
|
+
for elem in data.elements:
|
|
259
|
+
escaped_desc = elem.description.replace('"', '\\"')
|
|
260
|
+
lines.append(f' {elem.element_type} {elem.name} := "{escaped_desc}";')
|
|
261
|
+
lines.append(" }")
|
|
262
|
+
|
|
263
|
+
# Penalty
|
|
264
|
+
if data.penalty:
|
|
265
|
+
lines.append("")
|
|
266
|
+
lines.append(" penalty {")
|
|
267
|
+
if data.penalty.has_imprisonment:
|
|
268
|
+
if data.penalty.imprisonment_min:
|
|
269
|
+
lines.append(f" imprisonment := {data.penalty.imprisonment_min} to {data.penalty.imprisonment_max};")
|
|
270
|
+
else:
|
|
271
|
+
lines.append(f" imprisonment := {data.penalty.imprisonment_max};")
|
|
272
|
+
if data.penalty.has_fine:
|
|
273
|
+
if data.penalty.fine_min:
|
|
274
|
+
lines.append(f" fine := {data.penalty.fine_min} to {data.penalty.fine_max};")
|
|
275
|
+
else:
|
|
276
|
+
lines.append(f" fine := {data.penalty.fine_max};")
|
|
277
|
+
if data.penalty.supplementary:
|
|
278
|
+
escaped = data.penalty.supplementary.replace('"', '\\"')
|
|
279
|
+
lines.append(f' supplementary := "{escaped}";')
|
|
280
|
+
lines.append(" }")
|
|
281
|
+
|
|
282
|
+
# Illustrations
|
|
283
|
+
if data.illustrations:
|
|
284
|
+
lines.append("")
|
|
285
|
+
lines.append(" illustrations {")
|
|
286
|
+
for illus in data.illustrations:
|
|
287
|
+
escaped = illus.description.replace('"', '\\"')
|
|
288
|
+
lines.append(f' {illus.label} "{escaped}";')
|
|
289
|
+
lines.append(" }")
|
|
290
|
+
|
|
291
|
+
lines.append("}")
|
|
292
|
+
|
|
293
|
+
return "\n".join(lines)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def run_wizard(
|
|
297
|
+
output: Optional[str] = None,
|
|
298
|
+
section: Optional[str] = None,
|
|
299
|
+
title: Optional[str] = None,
|
|
300
|
+
verbose: bool = False,
|
|
301
|
+
color: bool = True,
|
|
302
|
+
) -> None:
|
|
303
|
+
"""
|
|
304
|
+
Run the interactive statute creation wizard.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
output: Output file path (None = stdout)
|
|
308
|
+
section: Pre-set section number
|
|
309
|
+
title: Pre-set title
|
|
310
|
+
verbose: Enable verbose output
|
|
311
|
+
color: Use colored output
|
|
312
|
+
"""
|
|
313
|
+
click.echo(click.style("""
|
|
314
|
+
╔═══════════════════════════════════════════════════════════╗
|
|
315
|
+
║ YUHO STATUTE CREATION WIZARD ║
|
|
316
|
+
║ ║
|
|
317
|
+
║ This wizard will guide you through creating a new ║
|
|
318
|
+
║ Yuho statute definition step by step. ║
|
|
319
|
+
╚═══════════════════════════════════════════════════════════╝
|
|
320
|
+
""", fg="cyan"))
|
|
321
|
+
|
|
322
|
+
data = StatuteData()
|
|
323
|
+
|
|
324
|
+
# Basic info
|
|
325
|
+
wizard_header("Basic Information")
|
|
326
|
+
|
|
327
|
+
data.section_number = section or prompt_input(
|
|
328
|
+
"Section number (e.g., 299, 300A)",
|
|
329
|
+
required=True
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
data.title = title or prompt_input(
|
|
333
|
+
"Statute title (e.g., 'Culpable Homicide')",
|
|
334
|
+
required=True
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
click.echo(click.style(f"\n✓ Creating statute: Section {data.section_number} - {data.title}", fg="green"))
|
|
338
|
+
|
|
339
|
+
# Collect sections
|
|
340
|
+
collect_definitions(data)
|
|
341
|
+
collect_elements(data)
|
|
342
|
+
collect_penalty(data)
|
|
343
|
+
collect_illustrations(data)
|
|
344
|
+
|
|
345
|
+
# Generate code
|
|
346
|
+
wizard_header("Generated Code")
|
|
347
|
+
code = generate_yuho_code(data)
|
|
348
|
+
|
|
349
|
+
click.echo(click.style("```yuho", fg="yellow"))
|
|
350
|
+
click.echo(code)
|
|
351
|
+
click.echo(click.style("```", fg="yellow"))
|
|
352
|
+
|
|
353
|
+
# Save or output
|
|
354
|
+
if output:
|
|
355
|
+
path = Path(output)
|
|
356
|
+
if path.exists() and not prompt_confirm(f"\nOverwrite existing file {path}?", default=False):
|
|
357
|
+
click.echo("Cancelled.")
|
|
358
|
+
return
|
|
359
|
+
|
|
360
|
+
path.write_text(code + "\n")
|
|
361
|
+
click.echo(click.style(f"\n✓ Saved to {path}", fg="green"))
|
|
362
|
+
else:
|
|
363
|
+
if prompt_confirm("\nSave to file?", default=True):
|
|
364
|
+
default_name = f"s{data.section_number.lower()}.yh"
|
|
365
|
+
filename = prompt_input("Filename", default=default_name)
|
|
366
|
+
path = Path(filename)
|
|
367
|
+
path.write_text(code + "\n")
|
|
368
|
+
click.echo(click.style(f"✓ Saved to {path}", fg="green"))
|
|
369
|
+
|
|
370
|
+
click.echo(click.style("\n✓ Wizard complete!", fg="green", bold=True))
|
yuho/cli/completions.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shell completion script generation for Yuho CLI.
|
|
3
|
+
|
|
4
|
+
Generates completion scripts for bash, zsh, and fish shells.
|
|
5
|
+
Uses Click's built-in completion support.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Literal
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
# Shell completion templates
|
|
12
|
+
|
|
13
|
+
BASH_COMPLETION_SCRIPT = '''
|
|
14
|
+
# Yuho bash completion script
|
|
15
|
+
# Add to ~/.bashrc or ~/.bash_completion:
|
|
16
|
+
# source <(yuho --show-completion bash)
|
|
17
|
+
# Or save to /etc/bash_completion.d/yuho
|
|
18
|
+
|
|
19
|
+
_yuho_completion() {
|
|
20
|
+
local IFS=$'\\n'
|
|
21
|
+
COMPREPLY=( $(env COMP_WORDS="${COMP_WORDS[*]}" \\
|
|
22
|
+
COMP_CWORD=$COMP_CWORD \\
|
|
23
|
+
_YUHO_COMPLETE=bash_complete $1) )
|
|
24
|
+
return 0
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
complete -o default -F _yuho_completion yuho
|
|
28
|
+
'''
|
|
29
|
+
|
|
30
|
+
ZSH_COMPLETION_SCRIPT = '''
|
|
31
|
+
#compdef yuho
|
|
32
|
+
# Yuho zsh completion script
|
|
33
|
+
# Add to ~/.zshrc:
|
|
34
|
+
# source <(yuho --show-completion zsh)
|
|
35
|
+
# Or save to a file in $fpath (e.g., ~/.zsh/completions/_yuho)
|
|
36
|
+
|
|
37
|
+
_yuho() {
|
|
38
|
+
local -a completions
|
|
39
|
+
local -a completions_with_descriptions
|
|
40
|
+
local -a response
|
|
41
|
+
(( ! $+commands[yuho] )) && return 1
|
|
42
|
+
|
|
43
|
+
response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) _YUHO_COMPLETE=zsh_complete yuho)}")
|
|
44
|
+
|
|
45
|
+
for key descr in ${(kv)response}; do
|
|
46
|
+
if [[ "$descr" == "_" ]]; then
|
|
47
|
+
completions+=("$key")
|
|
48
|
+
else
|
|
49
|
+
completions_with_descriptions+=("$key":"$descr")
|
|
50
|
+
fi
|
|
51
|
+
done
|
|
52
|
+
|
|
53
|
+
if [[ -n $googlemock_completions_with_descriptions ]]; then
|
|
54
|
+
_describe -V unsorted completions_with_descriptions -U
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
if [[ -n $completions ]]; then
|
|
58
|
+
compadd -U -V unsorted -a completions
|
|
59
|
+
fi
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
compdef _yuho yuho
|
|
63
|
+
'''
|
|
64
|
+
|
|
65
|
+
FISH_COMPLETION_SCRIPT = '''
|
|
66
|
+
# Yuho fish completion script
|
|
67
|
+
# Add to ~/.config/fish/completions/yuho.fish
|
|
68
|
+
# Or source directly: yuho --show-completion fish | source
|
|
69
|
+
|
|
70
|
+
function _yuho_completion
|
|
71
|
+
set -l response (env _YUHO_COMPLETE=fish_complete COMP_WORDS=(commandline -cp) COMP_CWORD=(commandline -t) yuho)
|
|
72
|
+
|
|
73
|
+
for completion in $response
|
|
74
|
+
set -l metadata (string split "," -- $completion)
|
|
75
|
+
set -l comp $metadata[1]
|
|
76
|
+
set -l desc ""
|
|
77
|
+
if test (count $metadata) -ge 2
|
|
78
|
+
set desc $metadata[2]
|
|
79
|
+
end
|
|
80
|
+
if test -z "$desc"
|
|
81
|
+
echo -e "$comp"
|
|
82
|
+
else
|
|
83
|
+
echo -e "$comp\\t$desc"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
complete -c yuho -f -a "(_yuho_completion)"
|
|
89
|
+
'''
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
ShellType = Literal["bash", "zsh", "fish"]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_completion_script(shell: ShellType) -> str:
|
|
96
|
+
"""
|
|
97
|
+
Get the shell completion script for the specified shell.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
shell: The shell type ("bash", "zsh", or "fish")
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
The completion script as a string
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
ValueError: If shell type is not supported
|
|
107
|
+
"""
|
|
108
|
+
scripts = {
|
|
109
|
+
"bash": BASH_COMPLETION_SCRIPT,
|
|
110
|
+
"zsh": ZSH_COMPLETION_SCRIPT,
|
|
111
|
+
"fish": FISH_COMPLETION_SCRIPT,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if shell not in scripts:
|
|
115
|
+
raise ValueError(
|
|
116
|
+
f"Unsupported shell: {shell}. Supported: {', '.join(scripts.keys())}"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return scripts[shell].strip()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def print_completion_script(shell: ShellType) -> None:
|
|
123
|
+
"""
|
|
124
|
+
Print the completion script for the specified shell.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
shell: The shell type
|
|
128
|
+
"""
|
|
129
|
+
click.echo(get_completion_script(shell))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def get_install_instructions(shell: ShellType) -> str:
|
|
133
|
+
"""
|
|
134
|
+
Get installation instructions for the completion script.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
shell: The shell type
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Installation instructions as a string
|
|
141
|
+
"""
|
|
142
|
+
instructions = {
|
|
143
|
+
"bash": """
|
|
144
|
+
# Bash completion installation:
|
|
145
|
+
# Option 1: Add to ~/.bashrc
|
|
146
|
+
echo 'eval "$(yuho completion bash)"' >> ~/.bashrc
|
|
147
|
+
|
|
148
|
+
# Option 2: Save to bash_completion.d (requires sudo)
|
|
149
|
+
yuho completion bash | sudo tee /etc/bash_completion.d/yuho
|
|
150
|
+
|
|
151
|
+
# Then restart your shell or run:
|
|
152
|
+
source ~/.bashrc
|
|
153
|
+
""",
|
|
154
|
+
"zsh": """
|
|
155
|
+
# Zsh completion installation:
|
|
156
|
+
# Option 1: Add to ~/.zshrc
|
|
157
|
+
echo 'eval "$(yuho completion zsh)"' >> ~/.zshrc
|
|
158
|
+
|
|
159
|
+
# Option 2: Save to fpath directory
|
|
160
|
+
mkdir -p ~/.zsh/completions
|
|
161
|
+
yuho completion zsh > ~/.zsh/completions/_yuho
|
|
162
|
+
# Then add to ~/.zshrc: fpath=(~/.zsh/completions $fpath)
|
|
163
|
+
|
|
164
|
+
# Then restart your shell or run:
|
|
165
|
+
source ~/.zshrc
|
|
166
|
+
""",
|
|
167
|
+
"fish": """
|
|
168
|
+
# Fish completion installation:
|
|
169
|
+
# Option 1: Save to completions directory
|
|
170
|
+
yuho completion fish > ~/.config/fish/completions/yuho.fish
|
|
171
|
+
|
|
172
|
+
# Option 2: Add to config.fish
|
|
173
|
+
echo 'yuho completion fish | source' >> ~/.config/fish/config.fish
|
|
174
|
+
|
|
175
|
+
# Completions will be available immediately in new shells
|
|
176
|
+
""",
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if shell not in instructions:
|
|
180
|
+
return f"No installation instructions for shell: {shell}"
|
|
181
|
+
|
|
182
|
+
return instructions[shell].strip()
|