janito 0.4.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.
- janito/__init__.py +1 -1
- janito/__main__.py +102 -326
- janito/agents/__init__.py +16 -0
- janito/agents/agent.py +21 -0
- janito/{claude.py → agents/claudeai.py} +13 -17
- janito/agents/openai.py +53 -0
- janito/agents/test.py +34 -0
- janito/change/__init__.py +32 -0
- janito/change/__main__.py +0 -0
- janito/change/analysis/__init__.py +23 -0
- janito/change/analysis/__main__.py +7 -0
- janito/change/analysis/analyze.py +61 -0
- janito/change/analysis/formatting.py +78 -0
- janito/change/analysis/options.py +81 -0
- janito/change/analysis/prompts.py +98 -0
- janito/change/analysis/view/__init__.py +9 -0
- janito/change/analysis/view/terminal.py +171 -0
- janito/change/applier/__init__.py +5 -0
- janito/change/applier/file.py +58 -0
- janito/change/applier/main.py +156 -0
- janito/change/applier/text.py +245 -0
- janito/change/applier/workspace_dir.py +58 -0
- janito/change/core.py +131 -0
- janito/change/history.py +44 -0
- janito/change/operations.py +7 -0
- janito/change/parser.py +289 -0
- janito/change/play.py +54 -0
- janito/change/preview.py +82 -0
- janito/change/prompts.py +126 -0
- janito/change/test.py +0 -0
- janito/change/validator.py +251 -0
- janito/change/viewer/__init__.py +11 -0
- janito/change/viewer/content.py +66 -0
- janito/change/viewer/diff.py +43 -0
- janito/change/viewer/pager.py +56 -0
- janito/change/viewer/panels.py +555 -0
- janito/change/viewer/styling.py +103 -0
- janito/change/viewer/themes.py +55 -0
- janito/clear_statement_parser/clear_statement_format.txt +328 -0
- janito/clear_statement_parser/examples.txt +326 -0
- janito/clear_statement_parser/models.py +104 -0
- janito/clear_statement_parser/parser.py +496 -0
- janito/cli/__init__.py +2 -0
- janito/cli/base.py +30 -0
- janito/cli/commands.py +45 -0
- janito/cli/functions.py +111 -0
- janito/cli/handlers/ask.py +22 -0
- janito/cli/handlers/demo.py +22 -0
- janito/cli/handlers/request.py +24 -0
- janito/cli/handlers/scan.py +9 -0
- janito/cli/history.py +61 -0
- janito/cli/registry.py +26 -0
- janito/common.py +41 -10
- janito/config.py +71 -6
- janito/demo/__init__.py +4 -0
- janito/demo/data.py +13 -0
- janito/demo/mock_data.py +20 -0
- janito/demo/operations.py +45 -0
- janito/demo/runner.py +59 -0
- janito/demo/scenarios.py +32 -0
- janito/prompts.py +1 -65
- janito/qa.py +8 -5
- janito/review.py +13 -0
- janito/search_replace/README.md +146 -0
- janito/search_replace/__init__.py +6 -0
- janito/search_replace/__main__.py +21 -0
- janito/search_replace/core.py +119 -0
- janito/search_replace/parser.py +52 -0
- janito/search_replace/play.py +61 -0
- janito/search_replace/replacer.py +36 -0
- janito/search_replace/searcher.py +299 -0
- janito/shell/__init__.py +39 -0
- janito/shell/bus.py +31 -0
- janito/shell/commands.py +195 -0
- janito/shell/handlers.py +122 -0
- janito/shell/history.py +20 -0
- janito/shell/processor.py +52 -0
- janito/tui/__init__.py +21 -0
- janito/tui/base.py +22 -0
- janito/tui/flows/__init__.py +5 -0
- janito/tui/flows/changes.py +65 -0
- janito/tui/flows/content.py +128 -0
- janito/tui/flows/selection.py +117 -0
- janito/tui/screens/__init__.py +3 -0
- janito/tui/screens/app.py +1 -0
- janito/workspace/__init__.py +7 -0
- janito/workspace/analysis.py +121 -0
- janito/workspace/manager.py +48 -0
- janito/workspace/scan.py +232 -0
- janito-0.6.0.dist-info/METADATA +185 -0
- janito-0.6.0.dist-info/RECORD +95 -0
- {janito-0.4.0.dist-info → janito-0.6.0.dist-info}/WHEEL +1 -1
- janito/analysis.py +0 -281
- janito/changeapplier.py +0 -436
- janito/changeviewer.py +0 -350
- janito/console.py +0 -330
- janito/contentchange.py +0 -84
- janito/contextparser.py +0 -113
- janito/fileparser.py +0 -125
- janito/scan.py +0 -137
- janito-0.4.0.dist-info/METADATA +0 -164
- janito-0.4.0.dist-info/RECORD +0 -21
- {janito-0.4.0.dist-info → janito-0.6.0.dist-info}/entry_points.txt +0 -0
- {janito-0.4.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
|
+
)
|