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}")