ezshell 0.2.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.
src/ui/prompt.py ADDED
@@ -0,0 +1,184 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ from prompt_toolkit import PromptSession
7
+ from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
8
+ from prompt_toolkit.completion import (
9
+ Completer,
10
+ Completion,
11
+ PathCompleter,
12
+ WordCompleter,
13
+ )
14
+ from prompt_toolkit.formatted_text import HTML
15
+ from prompt_toolkit.history import FileHistory
16
+ from prompt_toolkit.key_binding import KeyBindings
17
+ from prompt_toolkit.styles import Style
18
+
19
+ from src.core.context import SystemContext
20
+
21
+ COMMON_COMMANDS = [
22
+ "git",
23
+ "docker",
24
+ "docker-compose",
25
+ "kubectl",
26
+ "terraform",
27
+ "python",
28
+ "python3",
29
+ "pip",
30
+ "pytest",
31
+ "npm",
32
+ "yarn",
33
+ "pnpm",
34
+ "cargo",
35
+ "rustc",
36
+ "go",
37
+ "curl",
38
+ "wget",
39
+ "ssh",
40
+ "scp",
41
+ "systemctl",
42
+ "journalctl",
43
+ "make",
44
+ "grep",
45
+ "find",
46
+ "cat",
47
+ "ls",
48
+ "cd",
49
+ "pwd",
50
+ "export",
51
+ "env",
52
+ "chmod",
53
+ "chown",
54
+ "tar",
55
+ "zip",
56
+ "unzip",
57
+ "df",
58
+ "du",
59
+ "top",
60
+ "htop",
61
+ ]
62
+
63
+ EZSHELL_BUILTINS = {
64
+ "!ask": "Ask AI a natural language terminal question",
65
+ "!explain": "Explain the last command output or error",
66
+ "!fix": "Re-execute the last suggested fix",
67
+ "!config": "View current ezShell configuration",
68
+ "!help": "Show ezShell documentation and built-in commands",
69
+ "exit": "Exit ezShell",
70
+ "quit": "Exit ezShell",
71
+ "clear": "Clear terminal screen",
72
+ }
73
+
74
+
75
+ class EzShellCompleter(Completer):
76
+ """Hybrid completer combining ezShell builtins, common commands, and filesystem paths."""
77
+
78
+ def __init__(self):
79
+ self.cmd_completer = WordCompleter(COMMON_COMMANDS, ignore_case=True)
80
+ self.path_completer = PathCompleter(expanduser=True)
81
+
82
+ def get_completions(self, document, complete_event):
83
+ text_before_cursor = document.text_before_cursor
84
+
85
+ # If user is typing an ezShell meta-command (!...)
86
+ if text_before_cursor.startswith("!"):
87
+ word = document.get_word_before_cursor()
88
+ for cmd, desc in EZSHELL_BUILTINS.items():
89
+ if cmd.startswith("!") and cmd.startswith(word):
90
+ yield Completion(
91
+ cmd,
92
+ start_position=-len(word),
93
+ display=cmd,
94
+ display_meta=desc,
95
+ )
96
+ return
97
+
98
+ # Offer command completion if at the beginning of the line
99
+ tokens = text_before_cursor.split()
100
+ if len(tokens) <= 1 and not text_before_cursor.endswith(" "):
101
+ yield from self.cmd_completer.get_completions(document, complete_event)
102
+
103
+ # Always offer path completion
104
+ yield from self.path_completer.get_completions(document, complete_event)
105
+
106
+
107
+ def get_prompt_text(context: SystemContext, last_exit_code: int = 0) -> HTML:
108
+ """Builds a high-contrast, vivid 2-line prompt with bright neon accents."""
109
+ user = context.current_user
110
+
111
+ # Compact home directory representation
112
+ cwd = context.current_directory
113
+ home = str(Path.home())
114
+ if cwd.startswith(home):
115
+ display_cwd = "~" + cwd[len(home):]
116
+ else:
117
+ display_cwd = cwd
118
+
119
+ # Truncate overly long paths
120
+ parts = display_cwd.split("/")
121
+ if len(parts) > 4:
122
+ display_cwd = f".../{parts[-2]}/{parts[-1]}"
123
+
124
+ # Git branch segment in vivid neon green
125
+ git_segment = ""
126
+ if context.git_branch:
127
+ dirty_indicator = '<style fg="#FFB800"><b>*</b></style>' if context.git_is_dirty else ""
128
+ git_segment = (
129
+ f' <style fg="#FFFFFF">on</style> '
130
+ f'<style fg="#00FF88"><b>⎇ {context.git_branch}</b></style>{dirty_indicator}'
131
+ )
132
+
133
+ # Exit code badge (vivid neon red on failure)
134
+ exit_badge = ""
135
+ if last_exit_code != 0:
136
+ exit_badge = f' <style fg="#FF2A6D"><b>[✘ {last_exit_code}]</b></style>'
137
+
138
+ arrow_color = "#00FF88" if last_exit_code == 0 else "#FF2A6D"
139
+
140
+ return HTML(
141
+ f'<style fg="#00E5FF"><b>╭─</b></style> '
142
+ f'<style fg="#C084FC"><b>{user}</b></style> '
143
+ f'<style fg="#FFFFFF">in</style> '
144
+ f'<style fg="#FACC15"><b>{display_cwd}</b></style>'
145
+ f'{git_segment}'
146
+ f'{exit_badge}\n'
147
+ f'<style fg="#00E5FF"><b>╰─</b></style><style fg="{arrow_color}"><b>❯</b></style> '
148
+ )
149
+
150
+
151
+ def create_prompt_session(history_file: Optional[Path] = None) -> PromptSession:
152
+ """Creates a configured prompt_toolkit PromptSession with high-contrast vibrant styles."""
153
+ hist_path = history_file or (Path.home() / ".ezshell_history")
154
+ hist_path.parent.mkdir(parents=True, exist_ok=True)
155
+
156
+ history = FileHistory(str(hist_path))
157
+ completer = EzShellCompleter()
158
+
159
+ style = Style.from_dict(
160
+ {
161
+ # Autocompletion popup styling with vibrant accents
162
+ "completion-menu": "bg:#0F172A #F8FAFC",
163
+ "completion-menu.completion": "bg:#0F172A #F8FAFC",
164
+ "completion-menu.completion.current": "bg:#00E5FF #0F172A bold",
165
+ "completion-menu.meta": "bg:#1E293B #94A3B8 italic",
166
+ # Fish-style auto-suggestions
167
+ "auto-suggestion": "#64748B italic",
168
+ }
169
+ )
170
+
171
+ kb = KeyBindings()
172
+
173
+ @kb.add("c-space")
174
+ def _insert_ask(event):
175
+ """Ctrl+Space inserts '!ask ' at prompt."""
176
+ event.current_buffer.insert_text("!ask ")
177
+
178
+ return PromptSession(
179
+ history=history,
180
+ completer=completer,
181
+ auto_suggest=AutoSuggestFromHistory(),
182
+ style=style,
183
+ key_bindings=kb,
184
+ )
src/ui/render.py ADDED
@@ -0,0 +1,269 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from typing import Optional
5
+
6
+ from rich.box import ROUNDED
7
+ from rich.console import Console, Group
8
+ from rich.markdown import Markdown
9
+ from rich.panel import Panel
10
+ from rich.rule import Rule
11
+ from rich.syntax import Syntax
12
+ from rich.table import Table
13
+ from rich.text import Text
14
+ from rich.theme import Theme
15
+
16
+ from src import __version__
17
+ from src.core.assistant import AskResult, DiagnosticResult
18
+ from src.core.safety import RiskLevel, SafetyReport
19
+
20
+ # Vivid neon palette — high-contrast, no muted tones
21
+ custom_theme = Theme(
22
+ {
23
+ "info": "#00E5FF bold", # electric cyan
24
+ "warning": "#FFB800 bold", # vivid amber
25
+ "error": "#FF2A6D bold", # neon red/pink
26
+ "success": "#00FF88 bold", # neon green
27
+ "command": "#FACC15 bold", # neon yellow
28
+ "muted": "#94A3B8", # cool grey (readable, not muddy)
29
+ "accent": "#C084FC bold", # vivid purple
30
+ "highlight": "#00E5FF bold", # electric cyan
31
+ }
32
+ )
33
+
34
+ console = Console(theme=custom_theme, file=sys.stdout)
35
+
36
+
37
+ def render_banner(model_name: str, shell_name: str) -> None:
38
+ """Renders a sleek, modern ezShell header banner."""
39
+ console.print()
40
+
41
+ title_text = Text()
42
+ title_text.append("ezShell ", style="bold #00E5FF")
43
+ title_text.append(f"v{__version__}", style="#94A3B8")
44
+ title_text.append(" • ", style="#475569")
45
+ title_text.append("Model: ", style="#94A3B8")
46
+ title_text.append(f"{model_name}", style="bold #C084FC")
47
+ title_text.append(" • ", style="#475569")
48
+ title_text.append("Shell: ", style="#94A3B8")
49
+ title_text.append(f"{shell_name}", style="#00FF88")
50
+
51
+ subtitle_text = Text()
52
+ subtitle_text.append("Type ", style="#94A3B8")
53
+ subtitle_text.append("!ask <query>", style="bold #00E5FF")
54
+ subtitle_text.append(" for instant AI help • ", style="#94A3B8")
55
+ subtitle_text.append("!help", style="bold #C084FC")
56
+ subtitle_text.append(" for commands • ", style="#94A3B8")
57
+ subtitle_text.append("exit", style="#FF2A6D")
58
+ subtitle_text.append(" to quit", style="#94A3B8")
59
+
60
+ content = Table.grid(padding=(0, 0))
61
+ content.add_row(title_text)
62
+ content.add_row(subtitle_text)
63
+
64
+ banner_panel = Panel(
65
+ content,
66
+ box=ROUNDED,
67
+ border_style="#00E5FF",
68
+ padding=(1, 2),
69
+ )
70
+ console.print(banner_panel)
71
+
72
+
73
+ def render_api_key_missing_card() -> None:
74
+ """Renders an attractive notice card when GROQ_API_KEY is not configured."""
75
+ text = Text()
76
+ text.append("Groq AI Engine Inactive\n", style="bold #FFB800")
77
+ text.append("ezShell will run commands normally, but automated error diagnosis is off.\n\n", style="#FFFFFF")
78
+ text.append("To activate AI assistance:\n", style="#94A3B8")
79
+ text.append(" 1. Add to .env.local: ", style="#94A3B8")
80
+ text.append("GROQ_API_KEY=gsk_...\n", style="bold #00E5FF")
81
+ text.append(" 2. Or configure via CLI: ", style="#94A3B8")
82
+ text.append("ezshell --set-key <key>", style="bold #C084FC")
83
+
84
+ panel = Panel(
85
+ text,
86
+ box=ROUNDED,
87
+ border_style="#FFB800",
88
+ padding=(0, 2),
89
+ )
90
+ console.print(panel)
91
+
92
+
93
+ def render_error_card(result: DiagnosticResult) -> None:
94
+ """Renders the AI diagnostic card matching the official ezShell reference design."""
95
+ console.print()
96
+
97
+ elements = []
98
+
99
+ # 1. User Query row: > explain this error
100
+ query_text = Text()
101
+ query_text.append("> ", style="bold #00E5FF")
102
+ query_text.append(f"explain this error: {result.command}", style="#F1F5F9")
103
+ elements.append(query_text)
104
+ elements.append(Text(""))
105
+
106
+ # 2. Section: CAUSE
107
+ cause_lbl = Text("CAUSE\n", style="bold #64748B")
108
+ cause_body = result.summary or result.explanation or "Unknown root cause."
109
+ elements.append(cause_lbl)
110
+ elements.append(Text(f"{cause_body}\n", style="#CAD5E2"))
111
+
112
+ # 3. Section: FIX
113
+ if result.suggested_fix:
114
+ fix_lbl = Text("FIX", style="bold #64748B")
115
+ elements.append(fix_lbl)
116
+
117
+ cmd_text = Text()
118
+ cmd_text.append("$ ", style="#64748B")
119
+ cmd_text.append(result.suggested_fix.strip(), style="bold #00E5FF")
120
+
121
+ fix_panel = Panel(
122
+ cmd_text,
123
+ box=ROUNDED,
124
+ border_style="#182739",
125
+ style="on #070D16",
126
+ padding=(0, 1),
127
+ )
128
+ elements.append(fix_panel)
129
+
130
+ # 4. Section: RISK
131
+ report = result.safety_report
132
+ if report:
133
+ if report.risk_level == RiskLevel.DANGEROUS:
134
+ dot_color = "#FF2A6D"
135
+ risk_label = "HIGH RISK"
136
+ elif report.risk_level == RiskLevel.WARNING:
137
+ dot_color = "#FFB800"
138
+ risk_label = "MEDIUM RISK"
139
+ else:
140
+ dot_color = "#00FF88"
141
+ risk_label = "LOW RISK"
142
+ reason = report.reason or "Only changes your local environment."
143
+ else:
144
+ dot_color = "#00FF88"
145
+ risk_label = "LOW RISK"
146
+ reason = "Only changes your local environment."
147
+
148
+ risk_text = Text()
149
+ risk_text.append("● ", style=f"bold {dot_color}")
150
+ risk_text.append(f"{risk_label} ", style=f"bold {dot_color}")
151
+ risk_text.append(f"{reason}\n", style="#7B8C9F")
152
+ elements.append(risk_text)
153
+
154
+ # 5. Divider & Footer
155
+ elements.append(Rule(style="#152232"))
156
+ elements.append(Text("ASK ANOTHER QUESTION\n", style="bold #64748B"))
157
+
158
+ footer = Text()
159
+ footer.append("[Enter run] ", style="bold #FFFFFF on #0E1724")
160
+ footer.append("[c copy] ", style="bold #FFFFFF on #0E1724")
161
+ footer.append("[e edit] ", style="bold #FFFFFF on #0E1724")
162
+ footer.append("[q close]", style="bold #FFFFFF on #0E1724")
163
+ elements.append(footer)
164
+
165
+ outer_panel = Panel(
166
+ Group(*elements),
167
+ title="[bold #00E5FF]>_[/bold #00E5FF] [bold #FFFFFF]ezShell[/bold #FFFFFF]",
168
+ subtitle="[dim #64748B][Esc] to close[/dim #64748B]",
169
+ border_style="#00E5FF",
170
+ box=ROUNDED,
171
+ padding=(1, 2),
172
+ )
173
+ console.print(outer_panel)
174
+ console.print()
175
+
176
+
177
+ def render_ask_card(result: AskResult) -> None:
178
+ """Renders the response card for an '!ask' query."""
179
+ console.print()
180
+
181
+ elements = []
182
+
183
+ if result.explanation:
184
+ elements.append(Markdown(result.explanation))
185
+
186
+ if result.suggested_fix:
187
+ fix_title = Text()
188
+ fix_title.append("\n› Recommended Command", style="bold #00E5FF")
189
+ if result.fix_description:
190
+ fix_title.append(f" ({result.fix_description})", style="italic #94A3B8")
191
+ elements.append(fix_title)
192
+
193
+ syntax = Syntax(
194
+ result.suggested_fix.strip(),
195
+ lexer="bash",
196
+ theme="monokai",
197
+ line_numbers=False,
198
+ padding=(1, 2),
199
+ background_color="#0F172A",
200
+ )
201
+ elements.append(syntax)
202
+
203
+ if result.safety_report and result.safety_report.risk_level != RiskLevel.SAFE:
204
+ level_style = "bold #FF2A6D" if result.safety_report.risk_level == RiskLevel.DANGEROUS else "bold #FFB800"
205
+ elements.append(
206
+ f"\n[{level_style}]SAFETY WARNING:[/{level_style}] {result.safety_report.reason}"
207
+ )
208
+
209
+ panel = Panel(
210
+ Group(*elements),
211
+ title=f"[bold #00E5FF] ? {result.question} [/bold #00E5FF]",
212
+ border_style="#00E5FF",
213
+ box=ROUNDED,
214
+ padding=(1, 2),
215
+ )
216
+ console.print(panel)
217
+
218
+
219
+ def render_info(message: str) -> None:
220
+ """Renders a styled info message."""
221
+ console.print(f"[#00E5FF]i[/] [#FFFFFF]{message}[/]")
222
+
223
+
224
+ def render_success(message: str) -> None:
225
+ """Renders a styled success message."""
226
+ console.print(f"[#00FF88]✔[/] [bold #FFFFFF]{message}[/]")
227
+
228
+
229
+ def render_warning(message: str) -> None:
230
+ """Renders a styled warning message."""
231
+ console.print(f"[#FFB800]![/] [bold #FFB800]{message}[/]")
232
+
233
+
234
+ def render_error(message: str) -> None:
235
+ """Renders a styled error message."""
236
+ console.print(f"[#FF2A6D]✖[/] [bold #FF2A6D]{message}[/]")
237
+
238
+
239
+ def prompt_fix_action(
240
+ fix_command: str,
241
+ safety_report: Optional[SafetyReport] = None,
242
+ ) -> str:
243
+ """Prompts the user to decide whether to run the suggested fix command.
244
+ Returns: 'run', 'edit', or 'skip'
245
+ """
246
+ if safety_report and safety_report.risk_level == RiskLevel.DANGEROUS:
247
+ console.print(
248
+ f"\n[bold #FF2A6D][!] DANGEROUS COMMAND DETECTED:[/] [bold #FFFFFF]{safety_report.reason}[/]"
249
+ )
250
+ try:
251
+ confirmation = console.input(
252
+ "Type [bold #FF2A6D]'CONFIRM'[/] to execute this command or press Enter to skip: "
253
+ ).strip()
254
+ return "run" if confirmation == "CONFIRM" else "skip"
255
+ except (KeyboardInterrupt, EOFError):
256
+ return "skip"
257
+
258
+ try:
259
+ choice = console.input(
260
+ "\n[bold #00FF88]› Execute fix?[/] [[bold #00E5FF]y[/]es / [bold #94A3B8]N[/]o / [bold #C084FC]e[/]dit]: "
261
+ ).strip().lower()
262
+
263
+ if choice in ("y", "yes"):
264
+ return "run"
265
+ elif choice in ("e", "edit"):
266
+ return "edit"
267
+ return "skip"
268
+ except (KeyboardInterrupt, EOFError):
269
+ return "skip"