repr-cli 0.1.0__py3-none-any.whl → 0.2.1__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.
repr/ui.py CHANGED
@@ -1,197 +1,60 @@
1
1
  """
2
- Rich terminal UI components for beautiful CLI output.
2
+ Terminal UI utilities for repr CLI.
3
+
4
+ Simple, focused output helpers using Rich.
3
5
  """
4
6
 
5
- from rich.console import Console, Group
7
+ from rich.console import Console
8
+ from rich.markdown import Markdown
6
9
  from rich.panel import Panel
7
- from rich.progress import (
8
- BarColumn,
9
- Progress,
10
- SpinnerColumn,
11
- TaskProgressColumn,
12
- TextColumn,
13
- TimeElapsedColumn,
14
- )
15
- from rich.style import Style
16
10
  from rich.table import Table
17
- from rich.text import Text
18
- from rich.markdown import Markdown
19
- from rich.live import Live
20
- from rich.layout import Layout
21
- from rich import box
22
-
23
- from . import __version__
24
- from .config import is_dev_mode
25
-
26
- # Console instance for consistent output
27
- console = Console()
11
+ from rich.progress import Progress, SpinnerColumn, TextColumn
12
+ from rich.prompt import Confirm
28
13
 
29
14
  # Brand colors
30
15
  BRAND_PRIMARY = "#6366f1" # Indigo
31
16
  BRAND_SUCCESS = "#22c55e" # Green
32
- BRAND_WARNING = "#eab308" # Yellow
17
+ BRAND_WARNING = "#f59e0b" # Amber
33
18
  BRAND_ERROR = "#ef4444" # Red
34
- BRAND_INFO = "#06b6d4" # Cyan
35
19
  BRAND_MUTED = "#6b7280" # Gray
36
20
 
21
+ # Console instance
22
+ console = Console()
23
+
37
24
 
38
25
  def print_header() -> None:
39
- """Print the branded CLI header."""
40
- header_text = Text()
41
- header_text.append("🚀 ", style="bold")
42
- header_text.append("ResumeFlow CLI", style=f"bold {BRAND_PRIMARY}")
43
- header_text.append(f" v{__version__}", style=BRAND_MUTED)
44
-
45
- panel = Panel(
46
- header_text,
47
- box=box.ROUNDED,
48
- border_style=BRAND_PRIMARY,
49
- padding=(0, 2),
50
- )
51
- console.print(panel)
26
+ """Print the repr header/banner."""
27
+ console.print()
28
+ console.print(f"[bold {BRAND_PRIMARY}]repr[/] - understand what you've actually worked on")
52
29
  console.print()
53
30
 
54
31
 
55
32
  def print_success(message: str) -> None:
56
33
  """Print a success message."""
57
- console.print(f"[bold {BRAND_SUCCESS}]✓[/] {message}")
34
+ console.print(f"[{BRAND_SUCCESS}]✓[/] {message}")
58
35
 
59
36
 
60
37
  def print_error(message: str) -> None:
61
38
  """Print an error message."""
62
- console.print(f"[bold {BRAND_ERROR}]✗[/] {message}")
39
+ console.print(f"[{BRAND_ERROR}]✗[/] {message}")
63
40
 
64
41
 
65
42
  def print_warning(message: str) -> None:
66
43
  """Print a warning message."""
67
- console.print(f"[bold {BRAND_WARNING}]⚠[/] {message}")
44
+ console.print(f"[{BRAND_WARNING}]⚠[/] {message}")
68
45
 
69
46
 
70
47
  def print_info(message: str) -> None:
71
48
  """Print an info message."""
72
- console.print(f"[{BRAND_INFO}]ℹ[/] {message}")
73
-
74
-
75
- def print_step(message: str, completed: bool = False, pending: bool = False) -> None:
76
- """Print a step indicator."""
77
- if completed:
78
- symbol = f"[bold {BRAND_SUCCESS}]✓[/]"
79
- elif pending:
80
- symbol = f"[{BRAND_MUTED}]○[/]"
81
- else:
82
- symbol = f"[bold {BRAND_PRIMARY}]●[/]"
83
-
84
- console.print(f" ├── {symbol} {message}")
85
-
86
-
87
- def print_last_step(message: str, completed: bool = False, pending: bool = False) -> None:
88
- """Print the last step indicator."""
89
- if completed:
90
- symbol = f"[bold {BRAND_SUCCESS}]✓[/]"
91
- elif pending:
92
- symbol = f"[{BRAND_MUTED}]○[/]"
93
- else:
94
- symbol = f"[bold {BRAND_PRIMARY}]●[/]"
95
-
96
- console.print(f" └── {symbol} {message}")
49
+ console.print(f"[{BRAND_MUTED}]ℹ[/] {message}")
97
50
 
98
51
 
99
52
  def print_next_steps(steps: list[str]) -> None:
100
- """Print next steps section."""
53
+ """Print next steps suggestions."""
101
54
  console.print()
102
55
  console.print("[bold]Next steps:[/]")
103
56
  for step in steps:
104
- console.print(f" [bold {BRAND_INFO}]→[/] {step}")
105
-
106
-
107
- def create_repo_table() -> Table:
108
- """Create a table for repository analysis display."""
109
- table = Table(
110
- box=box.ROUNDED,
111
- border_style=BRAND_MUTED,
112
- show_header=True,
113
- header_style="bold",
114
- )
115
- table.add_column("Repository", style="bold")
116
- table.add_column("Language", style=BRAND_INFO)
117
- table.add_column("Commits", justify="right")
118
- table.add_column("Age", justify="right")
119
- table.add_column("Status", justify="center")
120
- return table
121
-
122
-
123
- def add_repo_row(
124
- table: Table,
125
- name: str,
126
- language: str | None = None,
127
- commits: int | None = None,
128
- age: str | None = None,
129
- status: str = "pending",
130
- ) -> None:
131
- """Add a row to the repository table."""
132
- status_map = {
133
- "pending": f"[{BRAND_MUTED}]○ Pending[/]",
134
- "analyzing": f"[bold {BRAND_PRIMARY}]● Analyzing...[/]",
135
- "completed": f"[bold {BRAND_SUCCESS}]✓[/]",
136
- "skipped": f"[{BRAND_MUTED}]⊘ skipped[/]",
137
- "error": f"[bold {BRAND_ERROR}]✗ Error[/]",
138
- }
139
-
140
- table.add_row(
141
- name,
142
- language or "-",
143
- str(commits) if commits else "-",
144
- age or "-",
145
- status_map.get(status, status),
146
- )
147
-
148
-
149
- def create_profile_table() -> Table:
150
- """Create a table for profile listing."""
151
- table = Table(
152
- box=box.ROUNDED,
153
- border_style=BRAND_MUTED,
154
- show_header=True,
155
- header_style="bold",
156
- )
157
- table.add_column("Profile", style="bold")
158
- table.add_column("Projects", justify="right")
159
- table.add_column("Size", justify="right")
160
- table.add_column("Status", justify="center")
161
- return table
162
-
163
-
164
- def create_analysis_progress() -> Progress:
165
- """Create a progress bar for analysis."""
166
- return Progress(
167
- SpinnerColumn(style=BRAND_PRIMARY),
168
- TextColumn("[bold]{task.description}"),
169
- BarColumn(bar_width=30, style=BRAND_MUTED, complete_style=BRAND_PRIMARY),
170
- TaskProgressColumn(),
171
- TimeElapsedColumn(),
172
- console=console,
173
- )
174
-
175
-
176
- def create_simple_progress() -> Progress:
177
- """Create a simple progress indicator."""
178
- return Progress(
179
- SpinnerColumn(style=BRAND_PRIMARY),
180
- TextColumn("[bold]{task.description}"),
181
- console=console,
182
- )
183
-
184
-
185
- def print_panel(title: str, content: str, border_color: str = BRAND_PRIMARY) -> None:
186
- """Print content in a panel."""
187
- panel = Panel(
188
- content,
189
- title=title,
190
- box=box.ROUNDED,
191
- border_style=border_color,
192
- padding=(1, 2),
193
- )
194
- console.print(panel)
57
+ console.print(f" {step}")
195
58
 
196
59
 
197
60
  def print_markdown(content: str) -> None:
@@ -200,47 +63,46 @@ def print_markdown(content: str) -> None:
200
63
  console.print(md)
201
64
 
202
65
 
203
- def print_profile_preview(content: str, max_lines: int = 15) -> None:
204
- """Print a preview of a profile."""
205
- lines = content.split("\n")
206
- preview = "\n".join(lines[:max_lines])
207
- if len(lines) > max_lines:
208
- preview += "\n\n..."
209
-
210
- panel = Panel(
211
- Markdown(preview),
212
- box=box.ROUNDED,
213
- border_style=BRAND_PRIMARY,
214
- padding=(1, 2),
215
- )
216
- console.print(panel)
66
+ def print_panel(title: str, content: str = "", border_color: str = BRAND_PRIMARY) -> None:
67
+ """Print a bordered panel."""
68
+ if content:
69
+ console.print(Panel(content, title=title, border_style=border_color))
70
+ else:
71
+ console.print(Panel(title, border_style=border_color))
217
72
 
218
73
 
219
74
  def print_auth_code(code: str) -> None:
220
- """Print the authentication code prominently."""
221
- code_text = Text(code, style=f"bold {BRAND_PRIMARY}")
222
- panel = Panel(
223
- code_text,
224
- box=box.ROUNDED,
225
- border_style=BRAND_PRIMARY,
226
- padding=(0, 4),
227
- )
75
+ """Print an auth code prominently."""
228
76
  console.print()
229
- console.print(panel, justify="center")
77
+ console.print(Panel(
78
+ f"[bold white on {BRAND_PRIMARY}] {code} [/]",
79
+ title="Enter this code",
80
+ border_style=BRAND_PRIMARY,
81
+ padding=(1, 4),
82
+ ))
230
83
  console.print()
231
84
 
232
85
 
233
- def print_connection_status(connected: bool) -> None:
234
- """Print WebSocket connection status."""
235
- host = "localhost:8003" if is_dev_mode() else "resumeflow.dev"
236
- if connected:
237
- console.print(f"Connected to {host} [{BRAND_SUCCESS}]●[/]")
238
- else:
239
- console.print(f"Disconnected from {host} [{BRAND_ERROR}]●[/]")
86
+ def create_spinner(message: str = "Working...") -> Progress:
87
+ """Create a spinner progress indicator."""
88
+ return Progress(
89
+ SpinnerColumn(),
90
+ TextColumn("[progress.description]{task.description}"),
91
+ console=console,
92
+ transient=True,
93
+ )
94
+
95
+
96
+ def create_table(title: str, columns: list[str]) -> Table:
97
+ """Create a styled table."""
98
+ table = Table(title=title, border_style=BRAND_MUTED)
99
+ for col in columns:
100
+ table.add_column(col)
101
+ return table
240
102
 
241
103
 
242
104
  def format_bytes(size: int) -> str:
243
- """Format bytes to human readable string."""
105
+ """Format bytes to human readable."""
244
106
  for unit in ["B", "KB", "MB", "GB"]:
245
107
  if size < 1024:
246
108
  return f"{size:.1f} {unit}"
@@ -248,183 +110,36 @@ def format_bytes(size: int) -> str:
248
110
  return f"{size:.1f} TB"
249
111
 
250
112
 
251
- def format_duration(months: int) -> str:
252
- """Format duration in months to human readable string."""
253
- if months < 1:
254
- return "< 1 mo"
255
- elif months < 12:
256
- return f"{months} mo"
257
- else:
258
- years = months // 12
259
- remaining_months = months % 12
260
- if remaining_months == 0:
261
- return f"{years} yr"
262
- return f"{years} yr {remaining_months} mo"
263
-
264
-
265
- class _AnimatedRenderable:
266
- """A renderable wrapper that calls the render function on each refresh."""
267
-
268
- def __init__(self, render_func):
269
- self.render_func = render_func
113
+ def format_relative_time(iso_date: str) -> str:
114
+ """Format ISO date as relative time."""
115
+ from datetime import datetime
270
116
 
271
- def __rich_console__(self, console, options):
272
- yield self.render_func()
273
-
274
-
275
- class AnalysisDisplay:
276
- """Live display for analysis progress with animations."""
277
-
278
- SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
279
- PROGRESS_CHARS = ["░", "▒", "▓", "█"]
280
-
281
- def __init__(self):
282
- self.repos: list[dict] = []
283
- self.current_step = ""
284
- self.current_detail = ""
285
- self.current_repo = ""
286
- self.progress_pct: float = 0.0
287
- self.connected = False
288
- self.live: Live | None = None
289
- self._frame: int = 0
290
- self._tick: int = 0
291
-
292
- def _get_spinner(self) -> str:
293
- return self.SPINNER_FRAMES[self._frame % len(self.SPINNER_FRAMES)]
294
-
295
- def _get_progress_bar(self, width: int = 30) -> Text:
296
- filled = int(self.progress_pct / 100 * width)
297
- shimmer_pos = self._tick % (width + 5)
298
-
299
- bar = Text()
300
- for i in range(width):
301
- if i < filled:
302
- if i == shimmer_pos or i == shimmer_pos - 1:
303
- bar.append("█", style=f"bold {BRAND_PRIMARY}")
304
- else:
305
- bar.append("█", style=BRAND_PRIMARY)
306
- elif i == filled and filled < width:
307
- partial_idx = int((self.progress_pct % (100/width)) / (100/width) * 4)
308
- bar.append(self.PROGRESS_CHARS[min(partial_idx, 3)], style=BRAND_PRIMARY)
309
- else:
310
- bar.append("░", style=BRAND_MUTED)
311
-
312
- return bar
313
-
314
- def _get_animated_status(self, status: str) -> str:
315
- if status == "analyzing":
316
- spinner = self._get_spinner()
317
- colors = [BRAND_PRIMARY, "#818cf8", "#a5b4fc", "#818cf8"]
318
- color = colors[self._frame % len(colors)]
319
- return f"[bold {color}]{spinner} Analyzing[/]"
320
- elif status == "pending":
321
- dots = "." * ((self._tick // 2) % 4)
322
- return f"[{BRAND_MUTED}]○ Waiting{dots}[/]"
323
- elif status == "completed":
324
- return f"[bold {BRAND_SUCCESS}]✓ Done[/]"
325
- elif status == "skipped":
326
- return f"[{BRAND_MUTED}]⊘ Skipped[/]"
327
- elif status == "error":
328
- return f"[bold {BRAND_ERROR}]✗ Error[/]"
329
- return status
330
-
331
- def _render(self):
332
- self._frame = (self._frame + 1) % len(self.SPINNER_FRAMES)
333
- self._tick += 1
117
+ try:
118
+ dt = datetime.fromisoformat(iso_date.replace("Z", "+00:00"))
119
+ now = datetime.now(dt.tzinfo)
120
+ delta = now - dt
334
121
 
335
- lines = []
336
-
337
- if self.current_step:
338
- spinner = self._get_spinner()
339
-
340
- # Map step names to nice icons
341
- step_icons = {
342
- "Starting": "🚀",
343
- "Extracting": "📂",
344
- "Preparing": "📋",
345
- "Analyzing": "🔍",
346
- "Synthesizing": "✨",
347
- "Merging": "🔗",
348
- "Finalizing": "📝",
349
- "Complete": "✅",
350
- }
351
- icon = step_icons.get(self.current_step, "")
352
-
353
- step_text = self.current_step.replace("_", " ").title()
354
- status_line = Text()
355
- status_line.append(f"{spinner} ", style=f"bold {BRAND_PRIMARY}")
356
- if icon:
357
- status_line.append(f"{icon} ", style="")
358
- status_line.append(step_text, style="bold")
359
-
360
- if self.current_repo:
361
- status_line.append(" → ", style=BRAND_MUTED)
362
- status_line.append(self.current_repo, style=BRAND_INFO)
363
-
364
- lines.append(status_line)
365
-
366
- if self.current_detail:
367
- detail_line = Text()
368
- detail_line.append(" ", style="")
369
- detail_line.append(self.current_detail, style=BRAND_MUTED)
370
- lines.append(detail_line)
371
-
372
- lines.append(Text())
373
-
374
- # Always show progress bar once we have a step (even at 0%)
375
- if self.current_step:
376
- progress_line = Text()
377
- progress_line.append(" ")
378
- progress_line.append_text(self._get_progress_bar(width=40))
379
- progress_line.append(f" {self.progress_pct:.0f}%", style=f"bold {BRAND_PRIMARY}")
380
- lines.append(progress_line)
381
- lines.append(Text())
382
-
383
- return Group(*lines) if lines else Text("")
384
-
385
- def start(self) -> None:
386
- """Start the live display."""
387
- # Use _AnimatedRenderable so Rich calls _render() on each refresh cycle
388
- self.live = Live(
389
- _AnimatedRenderable(self._render),
390
- console=console,
391
- refresh_per_second=10,
392
- transient=False,
393
- )
394
- self.live.start()
395
-
396
- def stop(self) -> None:
397
- """Stop the live display."""
398
- if self.live:
399
- self.live.stop()
400
-
401
- def update_progress(
402
- self,
403
- step: str | None = None,
404
- detail: str | None = None,
405
- repo: str | None = None,
406
- progress: float | None = None,
407
- ) -> None:
408
- """Update progress information."""
409
- if step is not None:
410
- self.current_step = step
411
- if detail is not None:
412
- self.current_detail = detail
413
- if repo is not None:
414
- self.current_repo = repo
415
- if progress is not None:
416
- self.progress_pct = progress
417
-
418
- def update_repo(self, name: str, **kwargs) -> None:
419
- """Update a repository's display status."""
420
- for repo in self.repos:
421
- if repo["name"] == name:
422
- repo.update(kwargs)
423
- break
122
+ if delta.days > 365:
123
+ years = delta.days // 365
124
+ return f"{years}y ago"
125
+ elif delta.days > 30:
126
+ months = delta.days // 30
127
+ return f"{months}mo ago"
128
+ elif delta.days > 0:
129
+ return f"{delta.days}d ago"
130
+ elif delta.seconds > 3600:
131
+ hours = delta.seconds // 3600
132
+ return f"{hours}h ago"
133
+ elif delta.seconds > 60:
134
+ mins = delta.seconds // 60
135
+ return f"{mins}m ago"
424
136
  else:
425
- self.repos.append({"name": name, **kwargs})
426
-
427
- def set_repos(self, repos: list[dict]) -> None:
428
- """Set all repositories."""
429
- self.repos = repos
137
+ return "just now"
138
+ except (ValueError, TypeError):
139
+ return iso_date[:10] if iso_date else "unknown"
140
+
141
+
142
+ def confirm(message: str, default: bool = False) -> bool:
143
+ """Prompt for confirmation."""
144
+ return Confirm.ask(message, default=default)
430
145