33from datetime import datetime
44from typing import Any
55
6+
67from rich .console import Console
78from rich .prompt import Prompt , Confirm
89from rich .panel import Panel
910from rich .table import Table
1011
12+ from prompt_toolkit import prompt
13+ from prompt_toolkit .history import FileHistory
14+ from prompt_toolkit .completion import PathCompleter , WordCompleter , FuzzyCompleter
15+
1116from .tree_view import print_json_analysis , print_compact_tree
1217from .search import JsonSearcher
1318from .stats import DataStatsAnalyzer
@@ -88,6 +93,87 @@ def run(self) -> int:
8893
8994 return 0
9095
96+ def _input (self , message : str , default : str | None = None , ** kwargs ) -> str :
97+ """
98+ User friendly input with optional choices and autocompletion.
99+ Falls back to Prompt.ask if prompt_toolkit is unavailable.
100+
101+ Args:
102+ message: Prompt message.
103+ default: Default value if user enters nothing.
104+ kwargs: May include 'choices' (list of strings) for tab completion.
105+
106+ Returns:
107+ User input as string (or default if empty).
108+ """
109+ choices = kwargs .get ("choices" )
110+ try :
111+
112+ history = FileHistory (".json_explorer_input_history" )
113+
114+ if choices :
115+ str_choices = [str (c ) for c in choices ]
116+ completer = FuzzyCompleter (WordCompleter (str_choices , ignore_case = True ))
117+ # Only show options in prompt, not above it
118+ display_message = f"{ message } ({ '/' .join (str_choices )} )"
119+
120+ while True :
121+ text = prompt (
122+ f"{ display_message } > " ,
123+ default = default or "" ,
124+ history = history ,
125+ completer = completer ,
126+ complete_while_typing = True ,
127+ ).strip ()
128+
129+ if not text and default is not None :
130+ return default
131+
132+ # Exact or case-insensitive match
133+ if text in str_choices :
134+ return text
135+ lowered = text .lower ()
136+ ci_matches = [c for c in str_choices if c .lower () == lowered ]
137+ if ci_matches :
138+ return ci_matches [0 ]
139+
140+ # Prefix match
141+ prefix_matches = [
142+ c for c in str_choices if c .lower ().startswith (lowered )
143+ ]
144+ if len (prefix_matches ) == 1 :
145+ return prefix_matches [0 ]
146+
147+ self .console .print (f"[red]Invalid choice: { text } [/red]" )
148+
149+ # Free text input
150+ return prompt (
151+ f"{ message } > " , default = default or "" , history = history
152+ ).strip () or (default or "" )
153+
154+ except Exception :
155+ return Prompt .ask (message , default = default , ** kwargs )
156+
157+ def _input_path (self , message : str ) -> str :
158+ """
159+ Input for file paths with autocompletion.
160+ Falls back to Prompt.ask if prompt_toolkit is unavailable.
161+ """
162+ try :
163+
164+ history = FileHistory (".json_explorer_path_history" )
165+ completer = PathCompleter (expanduser = True )
166+
167+ return prompt (
168+ f"{ message } > " ,
169+ history = history ,
170+ completer = completer ,
171+ complete_while_typing = True ,
172+ ).strip ()
173+
174+ except Exception :
175+ return Prompt .ask (message )
176+
91177 def _show_main_menu (self ) -> None :
92178 """Display the main menu."""
93179 menu_panel = Panel .fit (
@@ -110,7 +196,7 @@ def _show_main_menu(self) -> None:
110196 def _interactive_tree_view (self ) -> None :
111197 """Interactive tree view options."""
112198 self .console .print ("\n 🌳 [bold]Tree View Options[/bold]" )
113- tree_type = Prompt . ask (
199+ tree_type = self . _input (
114200 "Select tree view type" ,
115201 choices = ["compact" , "analysis" , "raw" ],
116202 default = "compact" ,
@@ -134,7 +220,7 @@ def _interactive_jmespath_search(self) -> None:
134220 self .searcher .print_examples ()
135221
136222 # Get query from user
137- query = Prompt . ask (
223+ query = self . _input (
138224 "\n [bold]Enter JMESPath query[/bold]" ,
139225 default = "@" , # @ returns entire document
140226 )
@@ -173,7 +259,7 @@ def _interactive_stats(self) -> None:
173259 def _interactive_visualization (self ) -> None :
174260 """Interactive visualization options."""
175261 self .console .print ("\n 📈 [bold]Visualization Options[/bold]" )
176- viz_format = Prompt . ask (
262+ viz_format = self . _input (
177263 "Select visualization format" ,
178264 choices = ["terminal" , "html" , "all" ],
179265 default = "html" ,
@@ -185,7 +271,7 @@ def _interactive_visualization(self) -> None:
185271
186272 if viz_format in ["html" , "all" ]:
187273 if Confirm .ask ("Save visualizations to file?" ):
188- save_path = Prompt . ask ("Enter save path (optional)" , default = "" )
274+ save_path = self . _input ("Enter save path (optional)" , default = "" )
189275 save_path = save_path if save_path else None
190276
191277 open_browser = Confirm .ask (
@@ -258,14 +344,16 @@ def _show_jmespath_help(self) -> None:
258344 def _load_new_data (self ) -> None :
259345 """Load new JSON data from file or URL."""
260346 self .console .print ("\n 📂 [bold]Load New Data[/bold]" )
261- source_type = Prompt .ask ("Data source" , choices = ["file" , "url" ], default = "file" )
347+ source_type = self ._input (
348+ "Data source" , choices = ["file" , "url" ], default = "file"
349+ )
262350
263351 try :
264352 if source_type == "file" :
265- file_path = Prompt . ask ("Enter file path" )
353+ file_path = self . _input_path ("Enter file path" )
266354 self .source , self .data = load_json (file_path , None )
267355 else :
268- url = Prompt . ask ("Enter URL" )
356+ url = self . _input ("Enter URL" )
269357 self .source , self .data = load_json (None , url )
270358
271359 self .console .print (f"✅ [green]Successfully loaded: { self .source } [/green]" )
@@ -302,7 +390,7 @@ def _save_search_result(self, result: Any) -> None:
302390 Args:
303391 result: SearchResult object to save.
304392 """
305- filename = Prompt . ask (
393+ filename = self . _input (
306394 "Enter filename" ,
307395 default = f"search_result_{ datetime .now ().strftime ('%Y%m%d_%H%M%S' )} .json" ,
308396 )
0 commit comments