janito 0.5.0__py3-none-any.whl → 0.6.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.
Files changed (106) hide show
  1. janito/__init__.py +0 -47
  2. janito/__main__.py +96 -15
  3. janito/agents/__init__.py +2 -8
  4. janito/agents/claudeai.py +3 -12
  5. janito/change/__init__.py +29 -16
  6. janito/change/__main__.py +0 -0
  7. janito/{analysis → change/analysis}/__init__.py +5 -15
  8. janito/change/analysis/__main__.py +7 -0
  9. janito/change/analysis/analyze.py +61 -0
  10. janito/change/analysis/formatting.py +78 -0
  11. janito/change/analysis/options.py +81 -0
  12. janito/{analysis → change/analysis}/prompts.py +35 -12
  13. janito/change/analysis/view/__init__.py +9 -0
  14. janito/change/analysis/view/terminal.py +171 -0
  15. janito/change/applier/__init__.py +5 -0
  16. janito/change/applier/file.py +58 -0
  17. janito/change/applier/main.py +156 -0
  18. janito/change/applier/text.py +245 -0
  19. janito/change/applier/workspace_dir.py +58 -0
  20. janito/change/core.py +131 -0
  21. janito/{changehistory.py → change/history.py} +12 -14
  22. janito/change/operations.py +7 -0
  23. janito/change/parser.py +289 -0
  24. janito/change/play.py +54 -0
  25. janito/change/preview.py +82 -0
  26. janito/change/prompts.py +126 -0
  27. janito/change/test.py +0 -0
  28. janito/change/validator.py +251 -0
  29. janito/{changeviewer → change/viewer}/__init__.py +3 -4
  30. janito/change/viewer/content.py +66 -0
  31. janito/{changeviewer → change/viewer}/diff.py +19 -4
  32. janito/change/viewer/pager.py +56 -0
  33. janito/change/viewer/panels.py +555 -0
  34. janito/change/viewer/styling.py +103 -0
  35. janito/{changeviewer → change/viewer}/themes.py +3 -5
  36. janito/clear_statement_parser/clear_statement_format.txt +328 -0
  37. janito/clear_statement_parser/examples.txt +326 -0
  38. janito/clear_statement_parser/models.py +104 -0
  39. janito/clear_statement_parser/parser.py +496 -0
  40. janito/cli/base.py +30 -0
  41. janito/cli/commands.py +30 -38
  42. janito/cli/functions.py +19 -194
  43. janito/cli/handlers/ask.py +22 -0
  44. janito/cli/handlers/demo.py +22 -0
  45. janito/cli/handlers/request.py +24 -0
  46. janito/cli/handlers/scan.py +9 -0
  47. janito/cli/history.py +61 -0
  48. janito/common.py +34 -3
  49. janito/config.py +71 -6
  50. janito/demo/__init__.py +4 -0
  51. janito/demo/data.py +13 -0
  52. janito/demo/mock_data.py +20 -0
  53. janito/demo/operations.py +45 -0
  54. janito/demo/runner.py +59 -0
  55. janito/demo/scenarios.py +32 -0
  56. janito/prompts.py +1 -80
  57. janito/qa.py +4 -3
  58. janito/search_replace/README.md +146 -0
  59. janito/search_replace/__init__.py +6 -0
  60. janito/search_replace/__main__.py +21 -0
  61. janito/search_replace/core.py +119 -0
  62. janito/search_replace/parser.py +52 -0
  63. janito/search_replace/play.py +61 -0
  64. janito/search_replace/replacer.py +36 -0
  65. janito/search_replace/searcher.py +299 -0
  66. janito/shell/__init__.py +39 -0
  67. janito/shell/bus.py +31 -0
  68. janito/shell/commands.py +195 -0
  69. janito/shell/handlers.py +122 -0
  70. janito/shell/history.py +20 -0
  71. janito/shell/processor.py +52 -0
  72. janito/tui/__init__.py +21 -0
  73. janito/tui/base.py +22 -0
  74. janito/tui/flows/__init__.py +5 -0
  75. janito/tui/flows/changes.py +65 -0
  76. janito/tui/flows/content.py +128 -0
  77. janito/tui/flows/selection.py +117 -0
  78. janito/tui/screens/__init__.py +3 -0
  79. janito/tui/screens/app.py +1 -0
  80. janito/workspace/__init__.py +7 -0
  81. janito/workspace/analysis.py +121 -0
  82. janito/workspace/manager.py +48 -0
  83. janito/workspace/scan.py +232 -0
  84. janito-0.6.0.dist-info/METADATA +185 -0
  85. janito-0.6.0.dist-info/RECORD +95 -0
  86. {janito-0.5.0.dist-info → janito-0.6.0.dist-info}/WHEEL +1 -1
  87. janito/_contextparser.py +0 -113
  88. janito/analysis/display.py +0 -149
  89. janito/analysis/options.py +0 -112
  90. janito/change/applier.py +0 -269
  91. janito/change/content.py +0 -62
  92. janito/change/indentation.py +0 -33
  93. janito/change/position.py +0 -169
  94. janito/changeviewer/panels.py +0 -268
  95. janito/changeviewer/styling.py +0 -59
  96. janito/console/__init__.py +0 -3
  97. janito/console/commands.py +0 -112
  98. janito/console/core.py +0 -62
  99. janito/console/display.py +0 -157
  100. janito/fileparser.py +0 -334
  101. janito/scan.py +0 -176
  102. janito/tests/test_fileparser.py +0 -26
  103. janito-0.5.0.dist-info/METADATA +0 -146
  104. janito-0.5.0.dist-info/RECORD +0 -45
  105. {janito-0.5.0.dist-info → janito-0.6.0.dist-info}/entry_points.txt +0 -0
  106. {janito-0.5.0.dist-info → janito-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,555 @@
1
+ from rich.console import Console
2
+ from rich.panel import Panel
3
+ from rich.columns import Columns
4
+ from rich import box
5
+ from rich.text import Text
6
+ from typing import List, Optional
7
+ from ..parser import FileChange, ChangeOperation
8
+ from .styling import format_content, create_legend_items
9
+ from .content import create_content_preview
10
+ from rich.rule import Rule
11
+ import shutil
12
+ import sys
13
+ from rich.live import Live
14
+ from .pager import check_pager # Add this import
15
+
16
+ # Remove clear_last_line, wait_for_space, and check_pager functions since they've been moved
17
+
18
+ def preview_all_changes(console: Console, changes: List[FileChange]) -> None:
19
+ """Show a summary of all changes with side-by-side comparison and continuous flow."""
20
+ """Show a summary of all changes with side-by-side comparison."""
21
+ total_changes = len(changes)
22
+
23
+ # Get terminal height
24
+ term_height = shutil.get_terminal_size().lines
25
+
26
+ # Show unified legend at the start
27
+ console.print(create_legend_items(console), justify="center")
28
+ console.print()
29
+
30
+ # Show progress indicator
31
+ with Live(console=console, refresh_per_second=4) as live:
32
+ live.update("[yellow]Processing changes...[/yellow]")
33
+
34
+ # Group changes by operation type
35
+ grouped_changes = {}
36
+ for change in changes:
37
+ if change.operation not in grouped_changes:
38
+ grouped_changes[change.operation] = []
39
+ grouped_changes[change.operation].append(change)
40
+
41
+ # Track content height
42
+ current_height = 2 # Account for legend and newline
43
+
44
+ # Show file operations with rule lines and track height
45
+ current_height = _show_file_operations(console, grouped_changes)
46
+
47
+ # Then show side-by-side panels for replacements
48
+ console.print("\n[bold blue]File Changes:[/bold blue]")
49
+ current_height += 2
50
+
51
+ for i, change in enumerate(changes):
52
+ if change.operation in (ChangeOperation.REPLACE_FILE, ChangeOperation.MODIFY_FILE):
53
+ show_side_by_side_diff(console, change, i, total_changes)
54
+
55
+ def _show_file_operations(console: Console, grouped_changes: dict) -> int:
56
+ """Display file operation summaries with content preview for new files.
57
+
58
+ Tracks current file being displayed and manages continuous flow.
59
+ """
60
+ """Display file operation summaries with content preview for new files."""
61
+ height = 0
62
+ for operation, group in grouped_changes.items():
63
+ for change in group:
64
+ if operation == ChangeOperation.CREATE_FILE:
65
+ console.print(Rule(f"[green]Creating new file: {change.name}[/green]", style="green"))
66
+ height += 1
67
+ if change.content:
68
+ preview = create_content_preview(change.name, change.content)
69
+ console.print(preview)
70
+ height += len(change.content.splitlines()) + 4 # Account for panel borders
71
+ elif operation == ChangeOperation.REMOVE_FILE:
72
+ console.print(Rule(f"[red]Removing file: {change.name}[/red]", style="red"))
73
+ height += 1
74
+ elif operation == ChangeOperation.RENAME_FILE:
75
+ console.print(Rule(f"[yellow]Renaming file: {change.name} → {change.target}[/yellow]", style="yellow"))
76
+ height += 1
77
+ elif operation == ChangeOperation.MOVE_FILE:
78
+ console.print(Rule(f"[blue]Moving file: {change.name} → {change.target}[/blue]", style="blue"))
79
+ height += 1
80
+ height = check_pager(console, height)
81
+ return height
82
+
83
+ def show_side_by_side_diff(console: Console, change: FileChange, change_index: int = 0, total_changes: int = 1) -> None:
84
+ """Show side-by-side diff panels for a file change with continuous flow.
85
+
86
+ Args:
87
+ console: Rich console instance
88
+ change: FileChange object containing the changes
89
+ change_index: Current change number (0-based)
90
+ total_changes: Total number of changes
91
+ """
92
+ """Show side-by-side diff panels for a file change with progress tracking and reason
93
+
94
+ Args:
95
+ console: Rich console instance
96
+ change: FileChange object containing the changes
97
+ change_index: Current change number (0-based)
98
+ total_changes: Total number of changes
99
+ """
100
+ # Track current file name to prevent unnecessary paging
101
+ from .pager import set_current_file, get_current_file
102
+ current_file = get_current_file()
103
+ new_file = str(change.name)
104
+
105
+ # Only update paging state for different files
106
+ if current_file != new_file:
107
+ set_current_file(new_file)
108
+ # Handle delete operations with special formatting
109
+ if change.operation == ChangeOperation.REMOVE_FILE:
110
+ show_delete_panel(console, change, change_index, total_changes)
111
+ return
112
+ # Get terminal width for layout decisions
113
+ term_width = console.width or 120
114
+ min_panel_width = 60 # Minimum width for readable content
115
+ can_do_side_by_side = term_width >= (min_panel_width * 2 + 4) # +4 for padding
116
+
117
+ # Get original and new content
118
+ original = change.original_content or ""
119
+ new_content = change.content or ""
120
+
121
+ # Split into lines
122
+ original_lines = original.splitlines()
123
+ new_lines = new_content.splitlines()
124
+
125
+ # Track accumulated height
126
+ current_height = 0
127
+
128
+ # Track content height
129
+ current_height += 1
130
+
131
+ # Check if we need to page before showing header
132
+ header_height = 3 # Account for panel borders and content
133
+ current_height = check_pager(console, current_height, header_height)
134
+
135
+ # Show the header with reason and progress
136
+ operation = change.operation.name.replace('_', ' ').title()
137
+ progress = f"Change {change_index + 1}/{total_changes}"
138
+ # Create centered reason text if present
139
+ reason_text = Text()
140
+ if change.reason:
141
+ reason_text.append("\n")
142
+ reason_text.append(change.reason, style="italic")
143
+ # Build header with operation and progress
144
+ header = Text()
145
+ header.append(f"{operation}:", style="bold cyan")
146
+ header.append(f" {change.name} ")
147
+ header.append(f"({progress})", style="dim")
148
+ header.append(reason_text)
149
+ # Display panel with centered content
150
+ console.print(Panel(header, box=box.HEAVY, style="cyan", title_align="center"))
151
+ current_height += header_height
152
+
153
+ # Show layout mode indicator
154
+ if not can_do_side_by_side:
155
+ console.print("[yellow]Terminal width is limited. Using vertical layout for better readability.[/yellow]")
156
+ console.print(f"[dim]Recommended terminal width: {min_panel_width * 2 + 4} or greater[/dim]")
157
+
158
+ # Handle text changes
159
+ if change.text_changes:
160
+ for text_change in change.text_changes:
161
+ search_lines = text_change.search_content.splitlines() if text_change.search_content else []
162
+ replace_lines = text_change.replace_content.splitlines() if text_change.replace_content else []
163
+
164
+ # Find modified sections
165
+ sections = find_modified_sections(search_lines, replace_lines)
166
+
167
+ # Show modification type and reason with rich rule
168
+ reason_text = f" - {text_change.reason}" if text_change.reason else ""
169
+ if text_change.search_content and text_change.replace_content:
170
+ console.print(Rule(f" Replace Text{reason_text} ", style="bold cyan", align="center"))
171
+ elif not text_change.search_content:
172
+ console.print(Rule(f" Append Text{reason_text} ", style="bold green", align="center"))
173
+ elif not text_change.replace_content:
174
+ console.print(Rule(f" Delete Text{reason_text} ", style="bold red", align="center"))
175
+
176
+ # Format and display each section
177
+ for i, (orig_section, new_section) in enumerate(sections):
178
+ left_panel = format_content(orig_section, orig_section, new_section, True)
179
+ right_panel = format_content(new_section, orig_section, new_section, False)
180
+
181
+ # Calculate upcoming content height
182
+ content_height = len(orig_section) + len(new_section) + 4 # Account for panel borders and padding
183
+
184
+ # Check if we need to page before showing content
185
+ current_height = check_pager(console, current_height, content_height)
186
+
187
+ # Create panels with adaptive width
188
+ if can_do_side_by_side:
189
+ # Calculate panel width for side-by-side layout
190
+ panel_width = (term_width - 4) // 2 # Account for padding
191
+ panels = [
192
+ Panel(
193
+ left_panel or "",
194
+ title="[red]Original Content[/red]",
195
+ title_align="center",
196
+ subtitle=str(change.name),
197
+ subtitle_align="center",
198
+ padding=(0, 1),
199
+ width=panel_width
200
+ ),
201
+ Panel(
202
+ right_panel or "",
203
+ title="[green]Modified Content[/green]",
204
+ title_align="center",
205
+ subtitle=str(change.name),
206
+ subtitle_align="center",
207
+ padding=(0, 1),
208
+ width=panel_width
209
+ )
210
+ ]
211
+
212
+ # Create columns with fixed width panels
213
+ columns = Columns(panels, equal=True, expand=False)
214
+ console.print()
215
+ console.print(columns, justify="center")
216
+ console.print()
217
+ else:
218
+ # Vertical layout for narrow terminals
219
+ panels = [
220
+ Panel(
221
+ left_panel or "",
222
+ title="[red]Original Content[/red]",
223
+ title_align="center",
224
+ subtitle=str(change.name),
225
+ subtitle_align="center",
226
+ padding=(0, 1),
227
+ width=term_width - 2
228
+ ),
229
+ Panel(
230
+ right_panel or "",
231
+ title="[green]Modified Content[/green]",
232
+ title_align="center",
233
+ subtitle=str(change.name),
234
+ subtitle_align="center",
235
+ padding=(0, 1),
236
+ width=term_width - 2
237
+ )
238
+ ]
239
+
240
+ # Display panels vertically
241
+ console.print()
242
+ for panel in panels:
243
+ console.print(panel, justify="center")
244
+ console.print()
245
+
246
+ # Show separator between sections if not last section
247
+ if i < len(sections) - 1:
248
+ console.print(Rule(" Section Break ", style="cyan dim", align="center"))
249
+
250
+ # Update height after displaying content
251
+ current_height += content_height
252
+ else:
253
+ # For non-text changes, show full content side by side
254
+ sections = find_modified_sections(original_lines, new_lines)
255
+ for i, (orig_section, new_section) in enumerate(sections):
256
+ left_panel = format_content(orig_section, orig_section, new_section, True)
257
+ right_panel = format_content(new_section, orig_section, new_section, False)
258
+
259
+ # Calculate content height for full file diff
260
+ content_height = len(orig_section) + len(new_section) + 4 # Account for panels
261
+ current_height = check_pager(console, current_height, content_height)
262
+
263
+ # Format content with appropriate width
264
+ left_panel = format_content(orig_section, orig_section, new_section, True)
265
+ right_panel = format_content(new_section, orig_section, new_section, False)
266
+
267
+ # Check terminal width for layout decision
268
+ term_width = console.width or 120
269
+ min_panel_width = 60 # Minimum width for readable content
270
+
271
+ # Determine if we can do side-by-side layout
272
+ can_do_side_by_side = term_width >= (min_panel_width * 2 + 4) # +4 for padding
273
+
274
+ if not can_do_side_by_side:
275
+ console.print("[yellow]Terminal width is limited. Using vertical layout for better readability.[/yellow]")
276
+ console.print(f"[dim]Recommended terminal width: {min_panel_width * 2 + 4} or greater[/dim]")
277
+
278
+ # Create unified header panel
279
+ header_text = Text()
280
+ header_text.append("[red]Original[/red]", style="bold")
281
+ header_text.append(" vs ")
282
+ header_text.append("[green]Modified[/green]", style="bold")
283
+ header_text.append(f" - {change.name}")
284
+
285
+ header_panel = Panel(
286
+ header_text,
287
+ box=box.HEAVY,
288
+ style="cyan",
289
+ padding=(0, 1)
290
+ )
291
+
292
+ # Create content panels without individual titles
293
+ panels = [
294
+ Panel(
295
+ left_panel,
296
+ padding=(0, 1),
297
+ width=None if can_do_side_by_side else term_width - 2
298
+ ),
299
+ Panel(
300
+ right_panel,
301
+ padding=(0, 1),
302
+ width=None if can_do_side_by_side else term_width - 2
303
+ )
304
+ ]
305
+
306
+ # Display unified header
307
+ console.print(header_panel, justify="center")
308
+
309
+ # Render panels based on layout
310
+ if can_do_side_by_side:
311
+ # Create centered columns with fixed width
312
+ available_width = console.width
313
+ panel_width = (available_width - 4) // 2 # Account for padding
314
+ for panel in panels:
315
+ panel.width = panel_width
316
+
317
+ columns = Columns(panels, equal=True, expand=False)
318
+ console.print(columns, justify="center")
319
+ else:
320
+ for panel in panels:
321
+ console.print(panel, justify="center")
322
+ console.print() # Add spacing between panels
323
+
324
+ # Show separator between sections if not last section
325
+ if i < len(sections) - 1:
326
+ console.print(Rule(style="dim"))
327
+
328
+ # Update height after displaying content
329
+ current_height += content_height
330
+
331
+ # Add final separator and success message
332
+ console.print(Rule(title="End Of Changes", style="bold blue"))
333
+ console.print()
334
+ console.print(Panel("[yellow]You're the best! All changes have been previewed successfully![/yellow]",
335
+ style="yellow",
336
+ title="Success",
337
+ title_align="center"))
338
+ console.print()
339
+
340
+ def find_modified_sections(original: list[str], modified: list[str], context_lines: int = 3) -> list[tuple[list[str], list[str]]]:
341
+ """
342
+ Find modified sections between original and modified text with surrounding context.
343
+ Merges sections with separator lines.
344
+
345
+ Args:
346
+ original: List of original lines
347
+ modified: List of modified lines
348
+ context_lines: Number of unchanged context lines to include
349
+
350
+ Returns:
351
+ List of tuples containing (original_section, modified_section) line pairs
352
+ """
353
+ # Find different lines
354
+ different_lines = set()
355
+ for i in range(max(len(original), len(modified))):
356
+ if i >= len(original) or i >= len(modified):
357
+ different_lines.add(i)
358
+ elif original[i] != modified[i]:
359
+ different_lines.add(i)
360
+
361
+ if not different_lines:
362
+ return []
363
+
364
+ # Group differences into sections with context
365
+ sections = []
366
+ current_section = set()
367
+
368
+ # Track original and modified content
369
+ orig_content = []
370
+ mod_content = []
371
+
372
+ for line_num in sorted(different_lines):
373
+ # If this line is far from current section, start new section
374
+ if not current_section or line_num <= max(current_section) + context_lines * 2:
375
+ current_section.add(line_num)
376
+ else:
377
+ # Process current section
378
+ start = max(0, min(current_section) - context_lines)
379
+ end = min(max(len(original), len(modified)),
380
+ max(current_section) + context_lines + 1)
381
+
382
+ # Add separator if not first section
383
+ if orig_content:
384
+ orig_content.append("...")
385
+ mod_content.append("...")
386
+
387
+ # Add section content
388
+ orig_content.extend(original[start:end])
389
+ mod_content.extend(modified[start:end])
390
+
391
+ current_section = {line_num}
392
+
393
+ # Process final section
394
+ if current_section:
395
+ start = max(0, min(current_section) - context_lines)
396
+ end = min(max(len(original), len(modified)),
397
+ max(current_section) + context_lines + 1)
398
+
399
+ # Add separator if needed
400
+ if orig_content:
401
+ orig_content.append("...")
402
+ mod_content.append("...")
403
+
404
+ # Add final section content
405
+ orig_content.extend(original[start:end])
406
+ mod_content.extend(modified[start:end])
407
+
408
+ # Return merged content as single section
409
+ return [(orig_content, mod_content)] if orig_content else []
410
+
411
+ def create_new_file_panel(name: Text, content: Text) -> Panel:
412
+ """Create a panel for new file creation with stats"""
413
+ stats = get_file_stats(content)
414
+ preview = create_content_preview(Path(str(name)), str(content))
415
+ return Panel(
416
+ preview,
417
+ title=f"[green]New File: {name}[/green]",
418
+ title_align="left",
419
+ subtitle=f"[dim]{stats}[/dim]",
420
+ subtitle_align="right",
421
+ box=box.HEAVY
422
+ )
423
+
424
+ def create_replace_panel(name: Text, change: FileChange) -> Panel:
425
+ """Create a panel for file replacement"""
426
+ original = change.original_content or ""
427
+ new_content = change.content or ""
428
+
429
+ term_width = Console().width or 120
430
+ panel_width = max(60, (term_width - 10) // 2)
431
+
432
+ panels = [
433
+ Panel(
434
+ format_content(original.splitlines(), original.splitlines(), new_content.splitlines(), True),
435
+ title="[red]Original Content[/red]",
436
+ title_align="left",
437
+ width=panel_width
438
+ ),
439
+ Panel(
440
+ format_content(new_content.splitlines(), original.splitlines(), new_content.splitlines(), False),
441
+ title="[green]New Content[/green]",
442
+ title_align="left",
443
+ width=panel_width
444
+ )
445
+ ]
446
+
447
+ return Panel(Columns(panels), title=f"[yellow]Replace: {name}[/yellow]", box=box.HEAVY)
448
+
449
+ def create_remove_file_panel(name: Text) -> Panel:
450
+ """Create a panel for file removal"""
451
+ return Panel(
452
+ "[red]This file will be removed[/red]",
453
+ title=f"[red]Remove File: {name}[/red]",
454
+ title_align="left",
455
+ box=box.HEAVY
456
+ )
457
+
458
+ def create_change_panel(search_content: Text, replace_content: Text, description: Text, width: int) -> Panel:
459
+ """Create a panel for text modifications"""
460
+ search_lines = search_content.splitlines() if search_content else []
461
+ replace_lines = replace_content.splitlines() if replace_content else []
462
+
463
+ term_width = Console().width or 120
464
+ panel_width = max(60, (term_width - 10) // width)
465
+
466
+ panels = [
467
+ Panel(
468
+ format_content(search_lines, search_lines, replace_lines, True),
469
+ title="[red]Search Content[/red]",
470
+ title_align="left",
471
+ width=panel_width
472
+ ),
473
+ Panel(
474
+ format_content(replace_lines, search_lines, replace_lines, False),
475
+ title="[green]Replace Content[/green]",
476
+ title_align="left",
477
+ width=panel_width
478
+ )
479
+ ]
480
+
481
+ return Panel(
482
+ Columns(panels),
483
+ title=f"[blue]Modification: {description}[/blue]",
484
+ box=box.HEAVY
485
+ )
486
+ def show_delete_panel(console: Console, change: FileChange, change_index: int = 0, total_changes: int = 1) -> None:
487
+ """Show a specialized panel for file deletion operations
488
+
489
+ Args:
490
+ console: Rich console instance
491
+ change: FileChange object containing the changes
492
+ change_index: Current change number (0-based)
493
+ total_changes: Total number of changes
494
+ """
495
+ # Track content height for panel display
496
+ current_height = 0
497
+
498
+ # Show the header with reason and progress
499
+ operation = change.operation.name.replace('_', ' ').title()
500
+ progress = f"Change {change_index + 1}/{total_changes}"
501
+
502
+ # Create centered reason text if present
503
+ reason_text = Text()
504
+ if change.reason:
505
+ reason_text.append("\n")
506
+ reason_text.append(change.reason, style="italic")
507
+
508
+ # Build header with operation and progress
509
+ header = Text()
510
+ header.append(f"{operation}:", style="bold red")
511
+ header.append(f" {change.name} ")
512
+ header.append(f"({progress})", style="dim")
513
+ header.append(reason_text)
514
+
515
+ # Display panel with centered content
516
+ console.print(Panel(header, box=box.HEAVY, style="red", title_align="center"))
517
+
518
+ # Create deletion panel
519
+ delete_text = Text()
520
+ delete_text.append("This file will be removed", style="bold red")
521
+ if change.original_content:
522
+ delete_text.append("\n\nOriginal file path: ", style="dim")
523
+ delete_text.append(str(change.name), style="red")
524
+
525
+ console.print(Panel(
526
+ delete_text,
527
+ title="[red]File Deletion[/red]",
528
+ title_align="center",
529
+ border_style="red",
530
+ padding=(1, 2)
531
+ ))
532
+
533
+ # Add final separator
534
+ console.print(Rule(title="End Of Changes", style="bold red"))
535
+ console.print()
536
+ import os
537
+ from typing import Union
538
+
539
+ def get_human_size(size_bytes: int) -> str:
540
+ """Convert bytes to human readable format"""
541
+ for unit in ['B', 'KB', 'MB', 'GB']:
542
+ if size_bytes < 1024.0:
543
+ return f"{size_bytes:.1f}{unit}"
544
+ size_bytes /= 1024.0
545
+ return f"{size_bytes:.1f}TB"
546
+
547
+ def get_file_stats(content: Union[str, Text]) -> str:
548
+ """Get file statistics in human readable format"""
549
+ if isinstance(content, Text):
550
+ lines = content.plain.splitlines()
551
+ size = len(content.plain.encode('utf-8'))
552
+ else:
553
+ lines = content.splitlines()
554
+ size = len(content.encode('utf-8'))
555
+ return f"{len(lines)} lines, {get_human_size(size)}"
@@ -0,0 +1,103 @@
1
+ from rich.text import Text
2
+ from rich.console import Console
3
+ from typing import List, Optional
4
+ from difflib import SequenceMatcher
5
+ from .diff import find_similar_lines, get_line_similarity
6
+ from .themes import DEFAULT_THEME, ColorTheme, ThemeType, get_theme_by_type
7
+
8
+ current_theme = DEFAULT_THEME
9
+
10
+ def set_theme(theme: ColorTheme) -> None:
11
+ """Set the current color theme"""
12
+ global current_theme
13
+ current_theme = theme
14
+
15
+ def format_content(lines: List[str], search_lines: List[str], replace_lines: List[str], is_search: bool, width: int = 80, is_delete: bool = False) -> Text:
16
+ """Format content with unified highlighting and indicators"""
17
+ text = Text()
18
+
19
+ # For delete operations, show all lines as deleted
20
+ if is_delete:
21
+ for line in lines:
22
+ bg_color = current_theme.line_backgrounds['deleted']
23
+ style = f"{current_theme.text_color} on {bg_color}"
24
+ content = f"✕ {line}"
25
+ padding = " " * max(0, width - len(content))
26
+ text.append("✕ ", style=style)
27
+ text.append(line, style=style)
28
+ text.append(padding, style=style)
29
+ text.append("\n", style=style)
30
+ return text
31
+
32
+ # Find similar lines for better diff visualization
33
+ similar_pairs = find_similar_lines(search_lines, replace_lines)
34
+ similar_added = {j for _, j, _ in similar_pairs}
35
+ similar_deleted = {i for i, _, _ in similar_pairs}
36
+
37
+ # Create sets for comparison
38
+ search_set = set(search_lines)
39
+ replace_set = set(replace_lines)
40
+ common_lines = search_set & replace_set
41
+
42
+ def add_line(line: str, prefix: str = " ", line_type: str = 'unchanged'):
43
+ bg_color = current_theme.line_backgrounds.get(line_type, current_theme.line_backgrounds['unchanged'])
44
+ style = f"{current_theme.text_color} on {bg_color}"
45
+
46
+ # Calculate padding to fill the width
47
+ content = f"{prefix} {line}"
48
+ padding = " " * max(0, width - len(content))
49
+
50
+ # Add prefix, content and padding with consistent background
51
+ text.append(f"{prefix} ", style=style)
52
+ text.append(line, style=style)
53
+ text.append(padding, style=style) # Add padding with same background
54
+ text.append("\n", style=style)
55
+
56
+ for i, line in enumerate(lines):
57
+ if not line.strip(): # Handle empty lines
58
+ add_line("", " ", 'unchanged')
59
+ elif line in common_lines:
60
+ add_line(line, " ", 'unchanged')
61
+ elif not is_search:
62
+ add_line(line, "✚", 'added')
63
+ else:
64
+ add_line(line, "✕", 'deleted')
65
+
66
+ return text
67
+
68
+ from rich.panel import Panel
69
+ from rich.columns import Columns
70
+
71
+ def create_legend_items(console: Console) -> Panel:
72
+ """Create a compact single panel with all legend items
73
+
74
+ Args:
75
+ console: Console instance for width calculation
76
+ """
77
+ text = Text()
78
+ term_width = console.width or 120
79
+
80
+ # Add unchanged item
81
+ unchanged_style = f"{current_theme.text_color} on {current_theme.line_backgrounds['unchanged']}"
82
+ text.append(" ", style=unchanged_style)
83
+ text.append(" Unchanged ", style=unchanged_style)
84
+
85
+ text.append(" ") # Spacing between items
86
+
87
+ # Add deleted item
88
+ deleted_style = f"{current_theme.text_color} on {current_theme.line_backgrounds['deleted']}"
89
+ text.append("✕", style=deleted_style)
90
+ text.append(" Deleted ", style=deleted_style)
91
+
92
+ text.append(" ") # Spacing between items
93
+
94
+ # Add added item
95
+ added_style = f"{current_theme.text_color} on {current_theme.line_backgrounds['added']}"
96
+ text.append("✚", style=added_style)
97
+ text.append(" Added", style=added_style)
98
+
99
+ return Panel(
100
+ text,
101
+ padding=(0, 1),
102
+ expand=False
103
+ )
@@ -10,16 +10,14 @@ class ThemeType(Enum):
10
10
  DARK_LINE_BACKGROUNDS = {
11
11
  'unchanged': '#1A1A1A',
12
12
  'added': '#003300',
13
- 'deleted': '#660000',
14
- 'modified': '#000030'
13
+ 'deleted': '#660000'
15
14
  }
16
15
 
17
16
  # Light theme colors
18
17
  LIGHT_LINE_BACKGROUNDS = {
19
18
  'unchanged': 'grey93', # Light gray for unchanged
20
19
  'added': 'light_green', # Semantic light green for additions
21
- 'deleted': 'light_red', # Semantic light red for deletions
22
- 'modified': 'light_blue' # Semantic light blue for modifications
20
+ 'deleted': 'light_red' # Semantic light red for deletions
23
21
  }
24
22
 
25
23
  @dataclass
@@ -45,7 +43,7 @@ DEFAULT_THEME = DARK_THEME
45
43
 
46
44
  def validate_theme(theme: ColorTheme) -> bool:
47
45
  """Validate that a theme contains all required colors"""
48
- required_line_backgrounds = {'unchanged', 'added', 'deleted', 'modified'}
46
+ required_line_backgrounds = {'unchanged', 'added', 'deleted'}
49
47
 
50
48
  if not isinstance(theme, ColorTheme):
51
49
  return False