1616from rich .table import Table
1717from 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+
1923from . 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"\n Select { 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 "\n What 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 ],
0 commit comments