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.
- janito/__init__.py +0 -47
- janito/__main__.py +105 -17
- janito/agents/__init__.py +9 -9
- janito/agents/agent.py +10 -3
- janito/agents/claudeai.py +15 -34
- janito/agents/openai.py +5 -1
- janito/change/__init__.py +29 -16
- janito/change/__main__.py +0 -0
- janito/{analysis → change/analysis}/__init__.py +5 -15
- janito/change/analysis/__main__.py +7 -0
- janito/change/analysis/analyze.py +62 -0
- janito/change/analysis/formatting.py +78 -0
- janito/change/analysis/options.py +81 -0
- janito/{analysis → change/analysis}/prompts.py +33 -18
- janito/change/analysis/view/__init__.py +9 -0
- janito/change/analysis/view/terminal.py +181 -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 +247 -0
- janito/change/applier/workspace_dir.py +58 -0
- janito/change/core.py +124 -0
- janito/{changehistory.py → change/history.py} +12 -14
- janito/change/operations.py +7 -0
- janito/change/parser.py +287 -0
- janito/change/play.py +54 -0
- janito/change/preview.py +82 -0
- janito/change/prompts.py +121 -0
- janito/change/test.py +0 -0
- janito/change/validator.py +269 -0
- janito/{changeviewer → change/viewer}/__init__.py +3 -4
- janito/change/viewer/content.py +66 -0
- janito/{changeviewer → change/viewer}/diff.py +19 -4
- janito/change/viewer/panels.py +533 -0
- janito/change/viewer/styling.py +114 -0
- janito/{changeviewer → change/viewer}/themes.py +3 -5
- 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/base.py +30 -0
- janito/cli/commands.py +75 -40
- janito/cli/functions.py +19 -194
- janito/cli/history.py +61 -0
- janito/common.py +65 -8
- janito/config.py +70 -5
- 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/prompt.py +36 -0
- janito/qa.py +6 -14
- janito/search_replace/README.md +192 -0
- janito/search_replace/__init__.py +7 -0
- janito/search_replace/__main__.py +21 -0
- janito/search_replace/core.py +120 -0
- janito/search_replace/logger.py +35 -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 +411 -0
- janito/search_replace/strategy_result.py +10 -0
- janito/shell/__init__.py +38 -0
- janito/shell/bus.py +31 -0
- janito/shell/commands.py +136 -0
- janito/shell/history.py +20 -0
- janito/shell/processor.py +32 -0
- janito/shell/prompt.py +48 -0
- janito/shell/registry.py +60 -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 +6 -0
- janito/workspace/analysis.py +121 -0
- janito/workspace/show.py +141 -0
- janito/workspace/stats.py +43 -0
- janito/workspace/types.py +98 -0
- janito/workspace/workset.py +108 -0
- janito/workspace/workspace.py +114 -0
- janito-0.7.0.dist-info/METADATA +167 -0
- janito-0.7.0.dist-info/RECORD +96 -0
- {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/WHEEL +1 -1
- janito/_contextparser.py +0 -113
- janito/analysis/display.py +0 -149
- janito/analysis/options.py +0 -112
- janito/change/applier.py +0 -269
- janito/change/content.py +0 -62
- janito/change/indentation.py +0 -33
- janito/change/position.py +0 -169
- janito/changeviewer/panels.py +0 -268
- janito/changeviewer/styling.py +0 -59
- janito/console/__init__.py +0 -3
- janito/console/commands.py +0 -112
- janito/console/core.py +0 -62
- janito/console/display.py +0 -157
- janito/fileparser.py +0 -334
- janito/prompts.py +0 -81
- janito/scan.py +0 -176
- janito/tests/test_fileparser.py +0 -26
- janito-0.5.0.dist-info/METADATA +0 -146
- janito-0.5.0.dist-info/RECORD +0 -45
- {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/entry_points.txt +0 -0
- {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'
|
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'
|
46
|
+
required_line_backgrounds = {'unchanged', 'added', 'deleted'}
|
49
47
|
|
50
48
|
if not isinstance(theme, ColorTheme):
|
51
49
|
return False
|