Skip to content

Commit 1d71307

Browse files
committed
better codegen UX
1 parent 1787b9a commit 1d71307

5 files changed

Lines changed: 185 additions & 45 deletions

File tree

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ __pycache__/
66
notes/
77
generated_examples/
88

9-
codegen_*_test.py
9+
codegen_*_test.py
10+
11+
.*_history

.json_explorer_input_history

Lines changed: 0 additions & 24 deletions
This file was deleted.

json_explorer/codegen/interactive.py

Lines changed: 104 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
from rich.table import Table
1717
from rich import box
1818

19+
from prompt_toolkit import prompt
20+
from prompt_toolkit.history import FileHistory
21+
from prompt_toolkit.completion import PathCompleter, WordCompleter, FuzzyCompleter
22+
1923
from . import (
2024
GeneratorError,
2125
generate_from_analysis,
@@ -122,6 +126,93 @@ def run_interactive(self) -> bool:
122126
logger.exception("Unexpected error in interactive handler")
123127
return False
124128

129+
# ========================================================================
130+
# prompt_toolkit integration
131+
# ========================================================================
132+
133+
def _input(self, message: str, default: str | None = None, **kwargs) -> str:
134+
"""
135+
User friendly input with optional choices and autocompletion.
136+
Falls back to Prompt.ask if prompt_toolkit is unavailable.
137+
138+
Args:
139+
message: Prompt message.
140+
default: Default value if user enters nothing.
141+
kwargs: May include 'choices' (list of strings) for tab completion.
142+
143+
Returns:
144+
User input as string (or default if empty).
145+
"""
146+
choices = kwargs.get("choices")
147+
try:
148+
149+
history = FileHistory(".json_explorer_input_history")
150+
151+
if choices:
152+
str_choices = [str(c) for c in choices]
153+
completer = FuzzyCompleter(WordCompleter(str_choices, ignore_case=True))
154+
# Only show options in prompt, not above it
155+
display_message = f"{message} ({'/'.join(str_choices)})"
156+
157+
while True:
158+
text = prompt(
159+
f"{display_message} > ",
160+
default=default or "",
161+
history=history,
162+
completer=completer,
163+
complete_while_typing=True,
164+
).strip()
165+
166+
if not text and default is not None:
167+
return default
168+
169+
# Exact or case-insensitive match
170+
if text in str_choices:
171+
return text
172+
lowered = text.lower()
173+
ci_matches = [c for c in str_choices if c.lower() == lowered]
174+
if ci_matches:
175+
return ci_matches[0]
176+
177+
# Prefix match
178+
prefix_matches = [
179+
c for c in str_choices if c.lower().startswith(lowered)
180+
]
181+
if len(prefix_matches) == 1:
182+
return prefix_matches[0]
183+
184+
self.console.print(f"[red]Invalid choice: {text}[/red]")
185+
186+
# Free text input
187+
return prompt(
188+
f"{message} > ", default=default or "", history=history
189+
).strip() or (default or "")
190+
191+
except Exception:
192+
return self._input(message, default=default, **kwargs)
193+
194+
def _input_path(self, message: str, **kwargs) -> str:
195+
"""
196+
Input for file paths with autocompletion.
197+
Falls back to Prompt.ask if prompt_toolkit is unavailable.
198+
"""
199+
default = kwargs.get("default")
200+
try:
201+
202+
history = FileHistory(".json_explorer_path_history")
203+
completer = PathCompleter(expanduser=True)
204+
205+
return prompt(
206+
f"{message} > ",
207+
default=default,
208+
history=history,
209+
completer=completer,
210+
complete_while_typing=True,
211+
).strip()
212+
213+
except Exception:
214+
return self._input(message)
215+
125216
# ========================================================================
126217
# Main Menu
127218
# ========================================================================
@@ -179,7 +270,7 @@ def _interactive_generation(self) -> None:
179270
return
180271

181272
# Step 3: Root name
182-
root_name = Prompt.ask("Root structure name", default="Root")
273+
root_name = self._input("Root structure name", default="Root")
183274

184275
# Step 4: Generate
185276
result = self._generate_code(language, config, root_name)
@@ -231,7 +322,7 @@ def _configure_generation(self, language: str) -> dict | None:
231322
"""Interactive configuration for code generation."""
232323
self.console.print(f"\n⚙️ [bold]Configure {language.title()} Generation[/bold]")
233324

234-
config_type = Prompt.ask(
325+
config_type = self._input(
235326
"Configuration approach",
236327
choices=["quick", "custom", "template", "file"],
237328
default="quick",
@@ -252,7 +343,7 @@ def _configure_generation(self, language: str) -> dict | None:
252343
def _quick_configuration(self, language: str) -> dict:
253344
"""Quick configuration with sensible defaults."""
254345
config_dict = {
255-
"package_name": Prompt.ask("Package/namespace name", default="main"),
346+
"package_name": self._input("Package/namespace name", default="main"),
256347
"add_comments": Confirm.ask("Generate comments?", default=True),
257348
}
258349

@@ -270,7 +361,7 @@ def _custom_configuration(self, language: str) -> dict:
270361
config_dict = {}
271362

272363
# Basic configuration
273-
config_dict["package_name"] = Prompt.ask(
364+
config_dict["package_name"] = self._input(
274365
"Package/namespace name",
275366
default="main",
276367
)
@@ -281,12 +372,12 @@ def _custom_configuration(self, language: str) -> dict:
281372

282373
# Naming conventions
283374
if Confirm.ask("Configure naming conventions?", default=False):
284-
config_dict["struct_case"] = Prompt.ask(
375+
config_dict["struct_case"] = self._input(
285376
"Struct/class name case",
286377
choices=["pascal", "camel", "snake"],
287378
default="pascal",
288379
)
289-
config_dict["field_case"] = Prompt.ask(
380+
config_dict["field_case"] = self._input(
290381
"Field name case",
291382
choices=["pascal", "camel", "snake"],
292383
default="pascal",
@@ -325,7 +416,7 @@ def _template_configuration(self, language: str) -> dict | None:
325416

326417
# Add custom option
327418
choices = list(templates.keys()) + ["custom", "back"]
328-
template = Prompt.ask(
419+
template = self._input(
329420
f"\nSelect {language} template",
330421
choices=choices,
331422
default=list(templates.keys())[0] if templates else "custom",
@@ -357,7 +448,7 @@ def _show_template_info(self, template_name: str, description: str) -> None:
357448

358449
def _file_configuration(self) -> dict | None:
359450
"""Load configuration from file."""
360-
config_file = Prompt.ask(
451+
config_file = self._input_path(
361452
"Configuration file path",
362453
default="codegen_config.json",
363454
)
@@ -446,7 +537,7 @@ def _handle_generation_output(
446537
self._display_metadata(result.metadata)
447538

448539
# Main output handling
449-
action = Prompt.ask(
540+
action = self._input(
450541
"\nWhat would you like to do with the generated code?",
451542
choices=["preview", "save", "both", "regenerate"],
452543
default="preview",
@@ -506,7 +597,7 @@ def _save_code(self, code: str, language: str, root_name: str) -> None:
506597

507598
# Suggest filename
508599
default_filename = f"{root_name.lower()}{extension}"
509-
filename = Prompt.ask("Save as", default=default_filename)
600+
filename = self._input_path("Save as", default=default_filename)
510601

511602
# Ensure proper extension
512603
if not filename.endswith(extension):
@@ -521,7 +612,7 @@ def _save_code(self, code: str, language: str, root_name: str) -> None:
521612
f"File {output_path} exists. Overwrite?",
522613
default=False,
523614
):
524-
filename = Prompt.ask("Enter new filename")
615+
filename = self._input_path("Enter new filename")
525616
output_path = Path(filename)
526617

527618
output_path.write_text(code, encoding="utf-8")
@@ -566,7 +657,7 @@ def _display_metadata(self, metadata: dict[str, Any]) -> None:
566657
def _show_languages_menu(self) -> None:
567658
"""Show detailed languages information menu."""
568659
while True:
569-
choice = Prompt.ask(
660+
choice = self._input(
570661
"\n[bold]Language Information[/bold]",
571662
choices=["list", "details", "specific", "back"],
572663
default="list",
@@ -638,7 +729,7 @@ def _show_specific_language_info(self) -> None:
638729
self.console.print("[red]No languages available[/red]")
639730
return
640731

641-
language = Prompt.ask(
732+
language = self._input(
642733
"Select language for detailed info",
643734
choices=languages + ["back"],
644735
default=languages[0],

json_explorer/codegen/languages/go/interactive.py

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
from rich.panel import Panel
1313
from rich.prompt import Prompt, Confirm
1414

15+
from prompt_toolkit import prompt
16+
from prompt_toolkit.history import FileHistory
17+
from prompt_toolkit.completion import WordCompleter, FuzzyCompleter
18+
1519
from ...core.config import GeneratorConfig
1620
from .config import get_web_api_config, get_strict_config, get_modern_config
1721

@@ -136,7 +140,7 @@ def configure_language_specific(
136140
"Add 'omitempty' to JSON tags?",
137141
default=True,
138142
)
139-
go_config["json_tag_case"] = Prompt.ask(
143+
go_config["json_tag_case"] = self._input(
140144
"JSON tag case style",
141145
choices=["original", "snake", "camel"],
142146
default="original",
@@ -150,25 +154,25 @@ def configure_language_specific(
150154

151155
# Type preferences
152156
if Confirm.ask("Configure type preferences?", default=False):
153-
go_config["int_type"] = Prompt.ask(
157+
go_config["int_type"] = self._input(
154158
"Integer type",
155159
choices=["int", "int32", "int64"],
156160
default="int64",
157161
)
158-
go_config["float_type"] = Prompt.ask(
162+
go_config["float_type"] = self._input(
159163
"Float type",
160164
choices=["float32", "float64"],
161165
default="float64",
162166
)
163-
go_config["unknown_type"] = Prompt.ask(
167+
go_config["unknown_type"] = self._input(
164168
"Unknown type representation",
165169
choices=["interface{}", "any"],
166170
default="interface{}",
167171
)
168172

169173
# Advanced options
170174
if Confirm.ask("Configure advanced options?", default=False):
171-
go_config["time_type"] = Prompt.ask(
175+
go_config["time_type"] = self._input(
172176
"Time type for timestamps",
173177
choices=["time.Time", "string", "int64"],
174178
default="time.Time",
@@ -274,3 +278,68 @@ def validate_config(self, config: dict[str, Any]) -> list[str]:
274278
logger.info(f"Config validation: {len(warnings)} warnings")
275279

276280
return warnings
281+
282+
# ========================================================================
283+
# prompt_toolkit integration
284+
# ========================================================================
285+
286+
def _input(self, message: str, default: str | None = None, **kwargs) -> str:
287+
"""
288+
User friendly input with optional choices and autocompletion.
289+
Falls back to Prompt.ask if prompt_toolkit is unavailable.
290+
291+
Args:
292+
message: Prompt message.
293+
default: Default value if user enters nothing.
294+
kwargs: May include 'choices' (list of strings) for tab completion.
295+
296+
Returns:
297+
User input as string (or default if empty).
298+
"""
299+
choices = kwargs.get("choices")
300+
try:
301+
302+
history = FileHistory(".json_explorer_input_history")
303+
304+
if choices:
305+
str_choices = [str(c) for c in choices]
306+
completer = FuzzyCompleter(WordCompleter(str_choices, ignore_case=True))
307+
# Only show options in prompt, not above it
308+
display_message = f"{message} ({'/'.join(str_choices)})"
309+
310+
while True:
311+
text = prompt(
312+
f"{display_message} > ",
313+
default=default or "",
314+
history=history,
315+
completer=completer,
316+
complete_while_typing=True,
317+
).strip()
318+
319+
if not text and default is not None:
320+
return default
321+
322+
# Exact or case-insensitive match
323+
if text in str_choices:
324+
return text
325+
lowered = text.lower()
326+
ci_matches = [c for c in str_choices if c.lower() == lowered]
327+
if ci_matches:
328+
return ci_matches[0]
329+
330+
# Prefix match
331+
prefix_matches = [
332+
c for c in str_choices if c.lower().startswith(lowered)
333+
]
334+
if len(prefix_matches) == 1:
335+
return prefix_matches[0]
336+
337+
self.console.print(f"[red]Invalid choice: {text}[/red]")
338+
339+
# Free text input
340+
return prompt(
341+
f"{message} > ", default=default or "", history=history
342+
).strip() or (default or "")
343+
344+
except Exception:
345+
return self._input(message, default=default, **kwargs)

0 commit comments

Comments
 (0)