From 3beb36b584cf02537f4dfe0720075d2794824afb Mon Sep 17 00:00:00 2001 From: VALLONGOL Date: Tue, 23 Dec 2025 11:04:03 +0100 Subject: [PATCH] aggiornarta la funzione di compare e la visualizzazione gui --- codebridge/core/comparer.py | 33 ++++- codebridge/gui/main_window.py | 243 ++++++++++++++++++++++++++++++---- 2 files changed, 245 insertions(+), 31 deletions(-) diff --git a/codebridge/core/comparer.py b/codebridge/core/comparer.py index 1e84340..58464d3 100644 --- a/codebridge/core/comparer.py +++ b/codebridge/core/comparer.py @@ -74,7 +74,7 @@ class CodeComparer: hash_sha256.update(chunk) return hash_sha256.hexdigest() - def get_relative_files(self, root_path: str) -> Dict[str, str]: + def get_relative_files(self, root_path: str, progress_callback=None) -> Dict[str, str]: """ Walks through the directory and returns a dict of relative paths and hashes. Filters out ignored patterns. @@ -89,23 +89,48 @@ class CodeComparer: full_path = os.path.join(root, file) relative_path = os.path.relpath(full_path, root_path) file_map[relative_path] = full_path + # notify progress for files scanned in source + try: + if progress_callback: + progress_callback(root_path, full_path, relative_path) + except Exception: + pass return file_map - def compare(self) -> Tuple[List[str], List[str], List[str]]: + def count_files(self, root_path: str) -> int: + """Quickly count non-ignored files under root_path.""" + total = 0 + for root, dirs, files in os.walk(root_path): + dirs[:] = [d for d in dirs if not self._is_ignored(d)] + for file in files: + if self._is_ignored(file): + continue + total += 1 + return total + + def compare(self, progress_callback=None) -> Tuple[List[str], List[str], List[str]]: """ Compares source and destination folders to find changes. """ self.added.clear() self.modified.clear() self.deleted.clear() - source_files = self.get_relative_files(self.source_path) - dest_files = self.get_relative_files(self.destination_path) + source_files = self.get_relative_files(self.source_path, progress_callback=progress_callback) + dest_files = self.get_relative_files(self.destination_path, progress_callback=None) source_keys: Set[str] = set(source_files.keys()) dest_keys: Set[str] = set(dest_files.keys()) self.added = sorted(list(source_keys - dest_keys)) self.deleted = sorted(list(dest_keys - source_keys)) common_files = source_keys.intersection(dest_keys) + # perform hashing comparison and report progress per file for relative_path in common_files: + # notify that we are hashing this file + try: + if progress_callback: + # indicate hashing phase with a simple tuple + progress_callback(("hash", relative_path)) + except Exception: + pass source_hash = self.compute_hash(source_files[relative_path]) dest_hash = self.compute_hash(dest_files[relative_path]) if source_hash != dest_hash: diff --git a/codebridge/gui/main_window.py b/codebridge/gui/main_window.py index f134db8..c9273c3 100644 --- a/codebridge/gui/main_window.py +++ b/codebridge/gui/main_window.py @@ -3,6 +3,7 @@ from tkinter import ttk, filedialog, messagebox from tkinter.scrolledtext import ScrolledText import os import logging +import threading # Externals (made importable by __main__.py adding their folders to sys.path) try: @@ -31,7 +32,13 @@ class MainWindow: def __init__(self, root: tk.Tk): self.root = root self.root.title("CodeBridge - Codebase Synchronizer") - self.root.geometry("900x600") + # Make the window taller by default so status/log area is visible + self.root.geometry("1000x760") + # Enforce a reasonable minimum size so status bar remains visible + try: + self.root.minsize(900, 600) + except Exception: + pass self.comparer = None self.sync_manager = SyncManager() # Initialize optional externals @@ -45,18 +52,19 @@ class MainWindow: except Exception: self.logger_sys = None - # Resource monitor + # Resource monitor (separate var) and progress status var self._resource_monitor = None - self.status_var = tk.StringVar() + self.resource_var = tk.StringVar() + self.progress_status_var = tk.StringVar() if TkinterResourceMonitor is not None and is_psutil_available(): try: - self._resource_monitor = TkinterResourceMonitor(self.root, self.status_var, poll_interval=1.0) + self._resource_monitor = TkinterResourceMonitor(self.root, self.resource_var, poll_interval=1.0) self._resource_monitor.start() except Exception: self._resource_monitor = None else: - # psutil not available -> show note - self.status_var.set("Resource monitor: psutil not installed") + # psutil not available -> show note on resource area + self.resource_var.set("Resource monitor: psutil not installed") # Profiles: load/save path in program root try: @@ -107,15 +115,38 @@ class MainWindow: self.dst_path_var = tk.StringVar() self._create_path_row(path_frame, "Source (New):", self.src_path_var, 0) self._create_path_row(path_frame, "Destination (Old):", self.dst_path_var, 1) - # Action buttons - btn_frame = ttk.Frame(self.root, padding=5) - btn_frame.pack(fill="x", padx=10) - compare_btn = ttk.Button(btn_frame, text="🔍 Compare Folders", command=self._run_comparison) - compare_btn.pack(side="left", padx=5) - export_btn = ttk.Button(btn_frame, text="📦 Export Changes (ZIP)", command=self._export_changes) - export_btn.pack(side="left", padx=5) - import_btn = ttk.Button(btn_frame, text="📥 Import Package (ZIP)", command=self._import_package) - import_btn.pack(side="left", padx=5) + # Action buttons inside a labelled frame and filters to the right + actions_row = ttk.Frame(self.root, padding=5) + actions_row.pack(fill="x", padx=10) + actions_frame = ttk.LabelFrame(actions_row, text="Actions", padding=6) + actions_frame.pack(side="left", fill="x", expand=True) + self.compare_btn = ttk.Button(actions_frame, text="🔍 Compare Folders", command=self._run_comparison) + self.compare_btn.pack(side="left", padx=5) + self.export_btn = ttk.Button(actions_frame, text="📦 Export Changes (ZIP)", command=self._export_changes) + self.export_btn.pack(side="left", padx=5) + self.import_btn = ttk.Button(actions_frame, text="📥 Import Package (ZIP)", command=self._import_package) + self.import_btn.pack(side="left", padx=5) + + # Filter panel on the right: show counts and allow toggling + filter_frame = ttk.Frame(actions_row, padding=(6,0)) + filter_frame.pack(side="right") + self.filter_vars = { + "added": tk.BooleanVar(value=False), + "modified": tk.BooleanVar(value=False), + "deleted": tk.BooleanVar(value=False), + } + # buttons that show counts; initial counts are 0 + self.filter_buttons = {} + self.filter_buttons["added"] = ttk.Button(filter_frame, text="Added (0)", command=lambda: self._toggle_filter("added")) + self.filter_buttons["added"].pack(side="left", padx=4) + self.filter_buttons["modified"] = ttk.Button(filter_frame, text="Modified (0)", command=lambda: self._toggle_filter("modified")) + self.filter_buttons["modified"].pack(side="left", padx=4) + self.filter_buttons["deleted"] = ttk.Button(filter_frame, text="Deleted (0)", command=lambda: self._toggle_filter("deleted")) + self.filter_buttons["deleted"].pack(side="left", padx=4) + # store last results for filtering + self.last_added = [] + self.last_modified = [] + self.last_deleted = [] # Results treeview self._setup_treeview() @@ -150,6 +181,10 @@ class MainWindow: """ tree_frame = ttk.Frame(self.root, padding=10) tree_frame.pack(fill="both", expand=True) + # Progress bar for long-running operations + self.progress_var = tk.IntVar(value=0) + self.progress = ttk.Progressbar(tree_frame, variable=self.progress_var, maximum=100, mode="determinate") + self.progress.pack(fill="x", pady=(0,6)) columns = ("status", "path") self.tree = ttk.Treeview(tree_frame, columns=columns, show="headings") self.tree.heading("status", text="Status") @@ -167,13 +202,89 @@ class MainWindow: # Persistent log box below the treeview log_frame = ttk.Frame(self.root, padding=(5, 2)) log_frame.pack(fill="x") - self.log_text = ScrolledText(log_frame, height=8, state=tk.DISABLED) + # Slightly reduce log height to keep status bar visible on smaller screens + self.log_text = ScrolledText(log_frame, height=6, state=tk.DISABLED) self.log_text.pack(fill="both", expand=True) - # Status bar at bottom + # Status bar at bottom: left = progress/status, right = resource monitor status_frame = ttk.Frame(self.root, padding=(5, 2)) status_frame.pack(fill="x", side="bottom") - ttk.Label(status_frame, textvariable=self.status_var).pack(side="left") + ttk.Label(status_frame, textvariable=self.progress_status_var).pack(side="left") + ttk.Label(status_frame, textvariable=self.resource_var).pack(side="right") + + def _log(self, message: str) -> None: + """Write message to both logging and the embedded ScrolledText log.""" + try: + import logging + logging.getLogger(__name__).info(message) + except Exception: + pass + + def _update_progress(self, value: int, maximum: int, phase: str, count_display: str = None) -> None: + try: + self.progress.config(maximum=maximum) + self.progress_var.set(value) + if count_display: + self.progress_status_var.set(f"{phase}: {count_display}") + else: + self.progress_status_var.set(f"{phase}: {value}/{maximum}") + except Exception: + pass + + def _toggle_filter(self, status: str) -> None: + """Toggle a filter button and refresh the treeview.""" + if status not in self.filter_vars: + return + cur = self.filter_vars[status].get() + self.filter_vars[status].set(not cur) + # update visual style of button to indicate active/inactive + try: + btn = self.filter_buttons.get(status) + if btn: + if self.filter_vars[status].get(): + btn.state(["pressed"]) + else: + btn.state(["!pressed"]) + except Exception: + pass + self._refresh_tree() + + def _refresh_tree(self) -> None: + """Repopulates the treeview according to current filter selections.""" + try: + for item in self.tree.get_children(): + self.tree.delete(item) + # gather active filters + active = {k for k, v in self.filter_vars.items() if v.get()} + # if no filter active, show all + show_all = len(active) == 0 + if show_all or "added" in active: + for f in self.last_added: + self.tree.insert("", "end", values=("Added", f), tags=("added",)) + if show_all or "modified" in active: + for f in self.last_modified: + self.tree.insert("", "end", values=("Modified", f), tags=("modified",)) + if show_all or "deleted" in active: + for f in self.last_deleted: + self.tree.insert("", "end", values=("Deleted", f), tags=("deleted",)) + except Exception as e: + self._log(f"Error refreshing tree: {e}") + + def _update_filter_counts(self) -> None: + """Update the text of filter buttons to reflect current counts.""" + try: + self.filter_buttons["added"].config(text=f"Added ({len(self.last_added)})") + self.filter_buttons["modified"].config(text=f"Modified ({len(self.last_modified)})") + self.filter_buttons["deleted"].config(text=f"Deleted ({len(self.last_deleted)})") + except Exception: + pass + try: + self.log_text.config(state=tk.NORMAL) + self.log_text.insert("end", message + "\n") + self.log_text.see("end") + self.log_text.config(state=tk.DISABLED) + except Exception: + pass # --- Profiles persistence --- def _load_profiles(self): @@ -198,11 +309,13 @@ class MainWindow: self.profile_cb.set(names[0]) def _open_manage_profiles(self): + self._log("Opening Manage Profiles dialog") dlg = ProfileManagerDialog(self.root, self._profiles_path, self.profiles) self.root.wait_window(dlg) # reload and refresh self.profiles = self._load_profiles() self._populate_profiles() + self._log("Profiles reloaded") def _close_profile_panel(self): try: @@ -226,6 +339,7 @@ class MainWindow: except Exception: pass self.current_ignore_exts = [] + self._log("Profile selection cleared") return profile = self.profiles.get(sel) if not profile: @@ -249,6 +363,7 @@ class MainWindow: self.dst_browse.config(state="disabled") except Exception: pass + self._log(f"Selected profile: {sel}") def _run_comparison(self) -> None: """ @@ -259,16 +374,90 @@ class MainWindow: if not src or not dst: messagebox.showwarning("Warning", "Please select both source and destination folders.") return + self._log(f"Starting comparison: src={src} dst={dst}") self.comparer = CodeComparer(src, dst, ignore_extensions=self.current_ignore_exts) - added, modified, deleted = self.comparer.compare() - for item in self.tree.get_children(): - self.tree.delete(item) - for f in added: - self.tree.insert("", "end", values=("Added", f), tags=("added",)) - for f in modified: - self.tree.insert("", "end", values=("Modified", f), tags=("modified",)) - for f in deleted: - self.tree.insert("", "end", values=("Deleted", f), tags=("deleted",)) + + def _disable_ui(): + try: + self.compare_btn.config(state="disabled") + self.export_btn.config(state="disabled") + self.import_btn.config(state="disabled") + except Exception: + pass + + def _enable_ui(): + try: + self.compare_btn.config(state="normal") + self.export_btn.config(state="normal") + self.import_btn.config(state="normal") + except Exception: + pass + + def worker(): + try: + # disable UI + self.root.after(0, _disable_ui) + # phase 1: quick count + try: + total = self.comparer.count_files(src) + except Exception: + total = 0 + if total <= 0: + total = 1 + progress_max = total * 2 + # initialize progress + self.root.after(0, lambda: self._update_progress(0, progress_max, "Counting", f"0/{total}")) + + scanned = 0 + hashed = 0 + + def progress_handler(*cb_args): + nonlocal scanned, hashed + # get_relative_files calls (root_path, full_path, relative_path) + if len(cb_args) == 3: + root_path, full_path, relative_path = cb_args + if root_path == src: + scanned += 1 + # update UI + self.root.after(0, lambda s=scanned: self._update_progress(s, progress_max, "Scanning", f"{s}/{total}")) + else: + # hashing phase: comparer sends a tuple like ("hash", relative_path) + arg = cb_args[0] if cb_args else None + if isinstance(arg, tuple) and arg and arg[0] == "hash": + hashed += 1 + v = scanned + hashed + self.root.after(0, lambda v=v: self._update_progress(v, progress_max, "Hashing", f"{hashed}/{total}")) + + # perform compare with progress handler + added, modified, deleted = self.comparer.compare(progress_callback=progress_handler) + + # ensure progress complete + self.root.after(0, lambda: self._update_progress(progress_max, progress_max, "Done", f"{total}/{total}")) + # populate tree in main thread + def finish_ui(): + # store results and refresh according to filters + self.last_added = added + self.last_modified = modified + self.last_deleted = deleted + self._update_filter_counts() + self._refresh_tree() + self._log(f"Comparison complete. Added={len(added)} Modified={len(modified)} Deleted={len(deleted)}") + self.progress_status_var.set("") + self.progress_var.set(0) + _enable_ui() + + self.root.after(0, finish_ui) + except Exception as e: + # ensure UI enabled on error + try: + self.root.after(0, _enable_ui) + except Exception: + pass + self._log(f"Error during comparison: {e}") + + threading.Thread(target=worker, daemon=True).start() + # Background comparison started; UI will be updated when finished. + self._log("Background comparison started") def _on_item_double_click(self, event) -> None: """