112 lines
4.4 KiB
Python
112 lines
4.4 KiB
Python
import tkinter as tk
|
|
from tkinter import ttk, messagebox
|
|
import difflib
|
|
import os
|
|
import shutil
|
|
|
|
|
|
class DiffViewer(tk.Toplevel):
|
|
"""
|
|
Side-by-side diff viewer for comparing two files.
|
|
"""
|
|
|
|
def __init__(self, parent, source_path: str, dest_path: str, relative_path: str):
|
|
super().__init__(parent)
|
|
self.source_path = source_path
|
|
self.dest_path = dest_path
|
|
self.relative_path = relative_path
|
|
self.title(f"Diff - {relative_path}")
|
|
self.geometry("1200x800")
|
|
self._setup_ui()
|
|
self._load_and_compare()
|
|
|
|
def _setup_ui(self) -> None:
|
|
"""
|
|
Initializes the side-by-side layout and buttons.
|
|
"""
|
|
# Toolbar
|
|
toolbar = ttk.Frame(self, padding=5)
|
|
toolbar.pack(fill="x", side="top")
|
|
copy_all_btn = ttk.Button(toolbar, text="⤴ Copy Source to Destination", command=self._copy_source_to_dest)
|
|
copy_all_btn.pack(side="left", padx=5)
|
|
# Main diff container
|
|
main_frame = ttk.Frame(self)
|
|
main_frame.pack(fill="both", expand=True, padx=5, pady=5)
|
|
# Left pane (Source)
|
|
self.left_text = tk.Text(main_frame, wrap="none", undo=True)
|
|
self.left_text.pack(side="left", fill="both", expand=True)
|
|
# Right pane (Destination)
|
|
self.right_text = tk.Text(main_frame, wrap="none", undo=True)
|
|
self.right_text.pack(side="left", fill="both", expand=True)
|
|
# Scrollbar for both
|
|
self.v_scroll = ttk.Scrollbar(main_frame, orient="vertical", command=self._sync_scroll)
|
|
self.v_scroll.pack(side="right", fill="y")
|
|
self.left_text.config(yscrollcommand=self.v_scroll.set)
|
|
self.right_text.config(yscrollcommand=self.v_scroll.set)
|
|
# Colors for diff
|
|
self.left_text.tag_config("removed", background="#ffeef0")
|
|
self.right_text.tag_config("added", background="#e6ffed")
|
|
self.left_text.tag_config("changed", background="#fffbdd")
|
|
self.right_text.tag_config("changed", background="#fffbdd")
|
|
|
|
def _sync_scroll(self, *args) -> None:
|
|
"""
|
|
Synchronizes vertical scrolling between both text widgets.
|
|
"""
|
|
self.left_text.yview(*args)
|
|
self.right_text.yview(*args)
|
|
|
|
def _load_and_compare(self) -> None:
|
|
"""
|
|
Reads files and highlights differences.
|
|
"""
|
|
try:
|
|
with open(self.source_path, "r", encoding="utf-8") as f:
|
|
source_lines = f.readlines()
|
|
# Handle case where destination file might not exist (Added files)
|
|
dest_lines = []
|
|
if os.path.exists(self.dest_path):
|
|
with open(self.dest_path, "r", encoding="utf-8") as f:
|
|
dest_lines = f.readlines()
|
|
diff = difflib.ndiff(dest_lines, source_lines)
|
|
self._display_diff(diff)
|
|
except Exception as e:
|
|
messagebox.showerror("Error", f"Could not read files: {e}")
|
|
self.destroy()
|
|
|
|
def _display_diff(self, diff_generator) -> None:
|
|
"""
|
|
Parses the diff and populates the text widgets with highlighting.
|
|
"""
|
|
self.left_text.config(state="normal")
|
|
self.right_text.config(state="normal")
|
|
for line in diff_generator:
|
|
code = line[:2]
|
|
content = line[2:]
|
|
if code == " ":
|
|
self.left_text.insert("end", content)
|
|
self.right_text.insert("end", content)
|
|
elif code == "- ":
|
|
self.left_text.insert("end", content, "removed")
|
|
self.right_text.insert("end", "\n")
|
|
elif code == "+ ":
|
|
self.left_text.insert("end", "\n")
|
|
self.right_text.insert("end", content, "added")
|
|
elif code == "? ":
|
|
continue
|
|
self.left_text.config(state="disabled")
|
|
self.right_text.config(state="disabled")
|
|
|
|
def _copy_source_to_dest(self) -> None:
|
|
"""
|
|
Overwrites the destination file with the source file content.
|
|
"""
|
|
confirm = messagebox.askyesno("Confirm", "Overwrite destination file with source content?")
|
|
if confirm:
|
|
try:
|
|
os.makedirs(os.path.dirname(self.dest_path), exist_ok=True)
|
|
shutil.copy2(self.source_path, self.dest_path)
|
|
messagebox.showinfo("Success", "File copied successfully.")
|
|
self.destroy()
|
|
except Exception as e:
|
|
messagebox.showerror("Error", f"Failed to copy file: {e}") |