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