Skip to content

Commit 1787b9a

Browse files
committed
better core UX
1 parent 2eeafa4 commit 1787b9a

2 files changed

Lines changed: 120 additions & 8 deletions

File tree

.json_explorer_input_history

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
2+
# 2025-12-04 16:38:49.907805
3+
+6
4+
5+
# 2025-12-04 16:38:51.097683
6+
+file
7+
8+
# 2025-12-04 16:39:04.127481
9+
+1
10+
11+
# 2025-12-04 16:48:09.272506
12+
+analysis
13+
14+
# 2025-12-04 16:52:09.838825
15+
+@
16+
17+
# 2025-12-04 16:52:53.991146
18+
+terminal
19+
20+
# 2025-12-04 16:53:05.888427
21+
+file
22+
23+
# 2025-12-04 16:55:19.276195
24+
+@

json_explorer/interactive.py

Lines changed: 96 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@
33
from datetime import datetime
44
from typing import Any
55

6+
67
from rich.console import Console
78
from rich.prompt import Prompt, Confirm
89
from rich.panel import Panel
910
from 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+
1116
from .tree_view import print_json_analysis, print_compact_tree
1217
from .search import JsonSearcher
1318
from .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

Comments
 (0)