comit-cli 1.0.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.
comit/ui.py ADDED
@@ -0,0 +1,412 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ from typing import Optional, Dict, Any, List, Tuple
6
+ from rich.console import Console
7
+ from rich.prompt import Prompt, Confirm
8
+
9
+ if sys.platform == "win32":
10
+ try:
11
+ if sys.stdout and hasattr(sys.stdout, "reconfigure"):
12
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
13
+ if sys.stderr and hasattr(sys.stderr, "reconfigure"):
14
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
15
+ except Exception:
16
+ pass
17
+
18
+ console = Console()
19
+ error_console = Console(stderr=True)
20
+
21
+
22
+ def show_step_success(message: str) -> None:
23
+ console.print(message)
24
+
25
+
26
+ def show_step_error(message: str) -> None:
27
+ error_console.print(f"Error: {message}")
28
+
29
+
30
+ def show_not_git_repository() -> None:
31
+ error_console.print("Not a Git repository.")
32
+
33
+
34
+ def show_no_staged_changes() -> None:
35
+ error_console.print("No staged changes found.\n")
36
+ error_console.print("Stage your changes first:")
37
+ error_console.print(" git add <files>\n")
38
+
39
+
40
+ def display_suggested_commit(message: str, title: str = "Generated") -> None:
41
+ console.print(f"{title}:\n\n {message}\n")
42
+
43
+
44
+ def _run_prompt_toolkit_menu(
45
+ options: List[Tuple[str, str]],
46
+ prompt_text: str = "",
47
+ default_index: int = 0,
48
+ allow_quit_key: bool = True,
49
+ ) -> Optional[str]:
50
+ from prompt_toolkit.application import Application
51
+ from prompt_toolkit.key_binding import KeyBindings
52
+ from prompt_toolkit.layout import Layout, HSplit, Window
53
+ from prompt_toolkit.layout.controls import FormattedTextControl
54
+ from prompt_toolkit.styles import Style
55
+
56
+ kb = KeyBindings()
57
+ selected = [default_index]
58
+ num_options = len(options)
59
+
60
+ @kb.add("up")
61
+ @kb.add("k")
62
+ def _(event):
63
+ selected[0] = (selected[0] - 1) % num_options
64
+
65
+ @kb.add("down")
66
+ @kb.add("j")
67
+ def _(event):
68
+ selected[0] = (selected[0] + 1) % num_options
69
+
70
+ @kb.add("enter")
71
+ def _(event):
72
+ event.app.exit(result=options[selected[0]][0])
73
+
74
+ if allow_quit_key:
75
+ @kb.add("q")
76
+ @kb.add("Q")
77
+ def _(event):
78
+ res = "cancel" if "cancel" in [o[0] for o in options] else "exit"
79
+ event.app.exit(result=res)
80
+
81
+ for opt_id, _ in options:
82
+ first_char = opt_id[0].lower()
83
+ @kb.add(first_char)
84
+ def _(event, val=opt_id):
85
+ event.app.exit(result=val)
86
+
87
+ for idx, (opt_id, _) in enumerate(options, start=1):
88
+ @kb.add(str(idx))
89
+ def _(event, val=opt_id):
90
+ event.app.exit(result=val)
91
+
92
+ @kb.add("c-c")
93
+ def _(event):
94
+ res = "cancel" if "cancel" in [o[0] for o in options] else "exit"
95
+ event.app.exit(result=res)
96
+
97
+ def get_formatted_text():
98
+ tokens = []
99
+ if prompt_text:
100
+ tokens.append(("bold", f"{prompt_text}\n\n"))
101
+ for i, (_, label) in enumerate(options):
102
+ if i == selected[0]:
103
+ tokens.append(("ansicyan bold", f"> {label}\n"))
104
+ else:
105
+ tokens.append(("", f" {label}\n"))
106
+ return tokens
107
+
108
+ style = Style.from_dict({
109
+ "prompt": "bold",
110
+ "cursor": "ansicyan bold",
111
+ "selected": "ansicyan bold",
112
+ "unselected": "",
113
+ })
114
+
115
+ app = Application(
116
+ layout=Layout(HSplit([Window(content=FormattedTextControl(get_formatted_text))])),
117
+ key_bindings=kb,
118
+ style=style,
119
+ full_screen=False,
120
+ erase_when_done=True,
121
+ )
122
+
123
+ return app.run()
124
+
125
+
126
+ def select_arrow_menu(
127
+ options: List[Tuple[str, str]],
128
+ prompt_text: str = "",
129
+ default_index: int = 0,
130
+ allow_quit_key: bool = True,
131
+ ) -> str:
132
+ if sys.stdin.isatty() and sys.stdout.isatty():
133
+ try:
134
+ result = _run_prompt_toolkit_menu(
135
+ options=options,
136
+ prompt_text=prompt_text,
137
+ default_index=default_index,
138
+ allow_quit_key=allow_quit_key,
139
+ )
140
+ if result:
141
+ return result
142
+ except Exception:
143
+ pass
144
+
145
+ if prompt_text:
146
+ console.print(f"{prompt_text}\n")
147
+ for idx, (opt_id, opt_label) in enumerate(options, start=1):
148
+ console.print(f" {opt_id} - {opt_label}")
149
+ choices = [opt_id for opt_id, _ in options]
150
+ if allow_quit_key:
151
+ choices.extend(["q", "quit"])
152
+ try:
153
+ choice = Prompt.ask(
154
+ "\nSelect an option",
155
+ default=options[default_index][0],
156
+ console=console,
157
+ ).strip().lower()
158
+ except Exception:
159
+ return options[default_index][0]
160
+
161
+ if allow_quit_key and choice in ("q", "quit"):
162
+ return "cancel" if "cancel" in [o[0] for o in options] else "exit"
163
+ for idx, (opt_id, opt_label) in enumerate(options, start=1):
164
+ if choice in (opt_id.lower(), opt_label.lower(), str(idx), opt_id[:1].lower()):
165
+ return opt_id
166
+ return options[default_index][0]
167
+
168
+
169
+ def prompt_action() -> str:
170
+ options = [
171
+ ("accept", "Accept"),
172
+ ("edit", "Edit"),
173
+ ("regenerate", "Regenerate"),
174
+ ("cancel", "Cancel"),
175
+ ]
176
+ return select_arrow_menu(options, prompt_text="", default_index=0)
177
+
178
+
179
+ def prompt_edit(current_message: str) -> Optional[str]:
180
+ console.print("\nEdit commit message (press Enter to save, Esc or Ctrl+C to cancel):")
181
+ while True:
182
+ try:
183
+ if sys.stdin.isatty() and sys.stdout.isatty():
184
+ from prompt_toolkit import prompt as pt_prompt
185
+ from prompt_toolkit.key_binding import KeyBindings
186
+
187
+ kb = KeyBindings()
188
+ cancelled = [False]
189
+
190
+ @kb.add("escape")
191
+ def _(event):
192
+ cancelled[0] = True
193
+ event.app.exit(result=None)
194
+
195
+ edited = pt_prompt("> ", default=current_message, key_bindings=kb)
196
+ if cancelled[0] or edited is None:
197
+ console.print("Edit cancelled. Keeping previous message.")
198
+ return None
199
+
200
+ cleaned = edited.strip()
201
+ if not cleaned:
202
+ error_console.print("Commit message cannot be empty. Please enter a valid message or press Esc to cancel.")
203
+ continue
204
+ return cleaned
205
+ else:
206
+ from rich.prompt import Prompt
207
+ edited = Prompt.ask("> ", default=current_message, console=console)
208
+ if edited is None:
209
+ return None
210
+ cleaned = edited.strip()
211
+ if not cleaned:
212
+ error_console.print("Commit message cannot be empty.")
213
+ return None
214
+ return cleaned
215
+ except (KeyboardInterrupt, EOFError):
216
+ console.print("Edit cancelled. Keeping previous message.")
217
+ return None
218
+ except Exception:
219
+ return None
220
+
221
+
222
+ def prompt_confirm_continue_commit() -> bool:
223
+ try:
224
+ return Confirm.ask(
225
+ "Continue to commit?",
226
+ default=False,
227
+ console=console,
228
+ )
229
+ except Exception:
230
+ return False
231
+
232
+
233
+ def show_commit_success(message: str) -> None:
234
+ console.print("\nCommit created successfully.\n")
235
+ console.print(f" {message}\n")
236
+
237
+
238
+ def show_auto_commit_success(message: str) -> None:
239
+ console.print(f"\nGenerated:\n\n {message}\n\nCommit created successfully.\n")
240
+
241
+
242
+ def show_cancelled() -> None:
243
+ console.print("\nCancelled. No commit was created.\n")
244
+
245
+
246
+ def show_error(message: str) -> None:
247
+ error_console.print(f"Error: {message}")
248
+
249
+
250
+ def show_settings_summary(summary: Dict[str, Any]) -> None:
251
+ console.print("\nComit Configuration\n")
252
+ console.print(f" AI Provider: {summary['provider']} ({summary.get('provider_source', 'default')})")
253
+ console.print(f" Model: {summary['model']} ({summary['model_source']})")
254
+ if summary['provider'] == "ollama":
255
+ console.print(f" Ollama Host: {summary['ollama_host']}")
256
+ else:
257
+ provider_title = summary['provider'].capitalize()
258
+ console.print(f" {provider_title} API Key: {summary['api_key_masked']} ({summary['api_key_source']})")
259
+ console.print(f" Config File: {summary['config_file']}")
260
+ console.print(f" Config File Status: {'Present' if summary['config_exists'] else 'Not created (using defaults)'}\n")
261
+
262
+
263
+ def prompt_settings_menu(provider: str = "groq") -> str:
264
+ options = [
265
+ ("view", "View configuration"),
266
+ ("set_provider", "Select AI provider"),
267
+ ("set_key", f"Configure {provider.capitalize()} API key" if provider != "ollama" else "Configure Ollama host"),
268
+ ("set_model", "Configure model"),
269
+ ("reset", "Reset configuration"),
270
+ ("exit", "Exit"),
271
+ ]
272
+ return select_arrow_menu(options, prompt_text="Settings Menu", default_index=0, allow_quit_key=True)
273
+
274
+
275
+ def prompt_provider_selection(current_provider: str = "groq") -> str:
276
+ options = [
277
+ ("groq", "Groq (Fast inference, default)"),
278
+ ("gemini", "Google Gemini"),
279
+ ("openai", "OpenAI"),
280
+ ("ollama", "Ollama (Local models)"),
281
+ ]
282
+ idx = 0
283
+ for i, (p_id, _) in enumerate(options):
284
+ if p_id == current_provider:
285
+ idx = i
286
+ break
287
+ return select_arrow_menu(options, prompt_text="Select AI Provider", default_index=idx, allow_quit_key=True)
288
+
289
+
290
+ def prompt_api_key(provider: str = "groq") -> str:
291
+ provider_title = provider.capitalize()
292
+ key = Prompt.ask(
293
+ f"Enter {provider_title} API Key",
294
+ password=True,
295
+ console=console,
296
+ ).strip()
297
+ return key
298
+
299
+
300
+ def prompt_model(current_model: str, provider: str = "groq") -> str:
301
+ provider_title = provider.capitalize()
302
+ if sys.stdin.isatty() and sys.stdout.isatty():
303
+ try:
304
+ from prompt_toolkit import prompt as pt_prompt
305
+ model = pt_prompt(f"Enter {provider_title} Model: ", default=current_model).strip()
306
+ return model if model else current_model
307
+ except Exception:
308
+ pass
309
+ model = Prompt.ask(
310
+ f"Enter {provider_title} Model",
311
+ default=current_model,
312
+ console=console,
313
+ ).strip()
314
+ return model if model else current_model
315
+
316
+
317
+ def prompt_ollama_host(current_host: str = "http://localhost:11434") -> str:
318
+ if sys.stdin.isatty() and sys.stdout.isatty():
319
+ try:
320
+ from prompt_toolkit import prompt as pt_prompt
321
+ host = pt_prompt("Enter Ollama Host: ", default=current_host).strip()
322
+ return host if host else current_host
323
+ except Exception:
324
+ pass
325
+ host = Prompt.ask(
326
+ "Enter Ollama Host",
327
+ default=current_host,
328
+ console=console,
329
+ ).strip()
330
+ return host if host else current_host
331
+
332
+
333
+ def prompt_confirm_reset() -> bool:
334
+ return Confirm.ask(
335
+ "Are you sure you want to reset all user configuration?",
336
+ default=False,
337
+ console=console,
338
+ )
339
+
340
+
341
+ def prompt_confirm_push() -> bool:
342
+ try:
343
+ return Confirm.ask(
344
+ "\nPush this commit to the remote?",
345
+ default=False,
346
+ console=console,
347
+ )
348
+ except Exception:
349
+ return False
350
+
351
+
352
+ def show_push_success(remote: str, branch: str) -> None:
353
+ console.print(f"Pushed to {remote}/{branch}.\n")
354
+
355
+
356
+ def show_push_skipped() -> None:
357
+ console.print("Commit created locally. Nothing was pushed.\n")
358
+
359
+
360
+ def show_no_remote(explicit_push: bool = False) -> None:
361
+ if explicit_push:
362
+ console.print("No Git remote is configured. Commit created locally; nothing was pushed.\n")
363
+ else:
364
+ console.print("No Git remote is configured. Commit created locally.\n")
365
+
366
+
367
+ def show_no_default_remote() -> None:
368
+ console.print("No default push remote ('origin') found. Commit created locally; nothing was pushed.\n")
369
+
370
+
371
+ def show_push_error(error_message: str) -> None:
372
+ error_console.print("\nPush failed:\n")
373
+ error_console.print(f" {error_message}\n")
374
+ console.print("Your commit was created locally and was not lost.\n")
375
+
376
+
377
+ def display_review_result(result: Any) -> None:
378
+ from comit.review.models import ReviewSeverity
379
+
380
+ if not result.has_findings:
381
+ console.print("No issues detected.\n")
382
+ return
383
+
384
+ console.print("Review findings:\n")
385
+
386
+ for finding in result.findings:
387
+ if finding.severity == ReviewSeverity.HIGH:
388
+ sev_tag = "HIGH"
389
+ elif finding.severity == ReviewSeverity.WARNING:
390
+ sev_tag = "WARNING"
391
+ else:
392
+ sev_tag = "INFO"
393
+
394
+ console.print(f"{sev_tag}")
395
+ console.print(f"{finding.message}")
396
+ console.print(f"{finding.file_path}")
397
+ if finding.details:
398
+ console.print(f" {finding.details}")
399
+ console.print()
400
+
401
+
402
+ def display_validation_result(result: Any) -> None:
403
+ if not result.has_warnings and not result.has_errors:
404
+ return
405
+
406
+ console.print("Validation warning:\n")
407
+ for finding in result.findings:
408
+ if finding.severity.value in ("WARNING", "ERROR"):
409
+ console.print(f" {finding.message}")
410
+ if finding.details:
411
+ console.print(f" {finding.details}")
412
+ console.print()