modificata la gui ed aggiunto sistema profili

This commit is contained in:
VALLONGOL 2025-12-23 10:32:22 +01:00
parent 9cea537345
commit ae53019b23
7 changed files with 523 additions and 14 deletions

View File

@ -1,4 +1,18 @@
import os
import sys
import tkinter as tk
# Ensure `externals` submodules are importable at runtime by adding
# their directories to sys.path. This keeps the externals as lightweight
# submodules without requiring packaging.
ROOT = os.path.dirname(os.path.dirname(__file__))
EXT_DIR = os.path.join(ROOT, "externals")
TKLOGGER_PATH = os.path.join(EXT_DIR, "python-tkinter-logger")
RESMON_PATH = os.path.join(EXT_DIR, "python-resource-monitor")
for p in (TKLOGGER_PATH, RESMON_PATH):
if os.path.isdir(p) and p not in sys.path:
sys.path.insert(0, p)
from codebridge.gui.main_window import MainWindow

View File

@ -21,9 +21,23 @@ class CodeComparer:
"thumbs.db"
}
def __init__(self, source_path: str, destination_path: str):
def __init__(self, source_path: str, destination_path: str, ignore_extensions=None):
self.source_path = source_path
self.destination_path = destination_path
# ignore_extensions: iterable of extensions (including dot), e.g. ['.o', '.d']
# Normalize: strip whitespace, ensure leading dot, lowercase
norm = set()
for e in (ignore_extensions or []):
try:
s = str(e).strip()
except Exception:
continue
if not s:
continue
if not s.startswith('.'):
s = '.' + s
norm.add(s.lower())
self.ignore_extensions = norm
self.added: List[str] = []
self.modified: List[str] = []
self.deleted: List[str] = []
@ -32,10 +46,22 @@ class CodeComparer:
"""
Checks if a file or directory name matches the ignore patterns.
"""
# exact-name ignores
if name in self.IGNORE_PATTERNS:
return True
if name.endswith(".pyc"):
# wildcard-like patterns in IGNORE_PATTERNS (simple suffix check for patterns like '*.pyc')
for pat in self.IGNORE_PATTERNS:
if pat.startswith("*") and name.lower().endswith(pat.lstrip("*").lower()):
return True
# explicit extension ignores provided at runtime
# compare case-insensitive
lname = name.lower()
for ext in self.ignore_extensions:
if ext and lname.endswith(ext):
return True
return False
def compute_hash(self, file_path: str) -> str:

View File

@ -27,9 +27,9 @@ class CommitDialog(tk.Toplevel):
self.text_area.pack(padx=10, pady=5, fill="both", expand=True)
btn_frame = ttk.Frame(self)
btn_frame.pack(fill="x", pady=10)
ok_btn = ttk.Button(btn_frame, text="Confirm", command=self._on_confirm)
ok_btn = ttk.Button(btn_frame, text="Confirm", command=self._on_confirm)
ok_btn.pack(side="right", padx=10)
cancel_btn = ttk.Button(btn_frame, text="Cancel", command=self.destroy)
cancel_btn = ttk.Button(btn_frame, text="Cancel", command=self.destroy)
cancel_btn.pack(side="right", padx=5)
def _on_confirm(self) -> None:

View File

@ -27,7 +27,7 @@ class DiffViewer(tk.Toplevel):
# 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 = 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)

View File

@ -1,10 +1,26 @@
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from tkinter.scrolledtext import ScrolledText
import os
import logging
# Externals (made importable by __main__.py adding their folders to sys.path)
try:
from tkinter_logger import TkinterLogger
except Exception:
TkinterLogger = None # type: ignore
try:
from resource_monitor import TkinterResourceMonitor, is_psutil_available
except Exception:
TkinterResourceMonitor = None # type: ignore
is_psutil_available = lambda: False
from codebridge.core.comparer import CodeComparer
from codebridge.core.sync_manager import SyncManager
from codebridge.gui.diff_viewer import DiffViewer
from codebridge.gui.commit_dialog import CommitDialog
from codebridge.gui.profile_dialog import ProfileManagerDialog, ProfileManagerFrame
import json
class MainWindow:
@ -18,7 +34,58 @@ class MainWindow:
self.root.geometry("900x600")
self.comparer = None
self.sync_manager = SyncManager()
# Initialize optional externals
self.logger_sys = None
if TkinterLogger is not None:
try:
self.logger_sys = TkinterLogger(self.root)
# enable console by default; tkinter handler will be added when log window opens
self.logger_sys.setup(enable_console=True, enable_tkinter=True)
logging.getLogger(__name__).info("TkinterLogger initialized")
except Exception:
self.logger_sys = None
# Resource monitor
self._resource_monitor = None
self.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.start()
except Exception:
self._resource_monitor = None
else:
# psutil not available -> show note
self.status_var.set("Resource monitor: psutil not installed")
# Profiles: load/save path in program root
try:
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
except Exception:
repo_root = os.getcwd()
self._profiles_path = os.path.join(repo_root, "profiles.json")
self.profiles = self._load_profiles()
self.current_ignore_exts = []
# Setup UI and close handler
self._setup_ui()
# Attach logger handler to persistent log widget if available
try:
if self.logger_sys and hasattr(self, "log_text"):
try:
self.logger_sys.add_tkinter_handler(self.log_text)
except Exception:
logging.getLogger(__name__).exception("Failed to attach tkinter log handler to main UI")
except Exception:
pass
# Populate profiles combobox if any
try:
self._populate_profiles()
except Exception:
pass
self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
def _setup_ui(self) -> None:
"""
@ -27,6 +94,15 @@ class MainWindow:
# Path selection frame
path_frame = ttk.LabelFrame(self.root, text="Path Selection", padding=10)
path_frame.pack(fill="x", padx=10, pady=5)
# Profiles combobox + Manage button
prof_frame = ttk.Frame(path_frame)
prof_frame.grid(row=0, column=0, columnspan=3, sticky="ew", pady=(0,6))
ttk.Label(prof_frame, text="Profile:").pack(side="left")
self.profile_var = tk.StringVar()
self.profile_cb = ttk.Combobox(prof_frame, textvariable=self.profile_var, state="readonly", width=40)
self.profile_cb.pack(side="left", padx=6)
self.profile_cb.bind("<<ComboboxSelected>>", lambda e: self._on_profile_change())
ttk.Button(prof_frame, text="⚙️ Manage Profiles...", command=self._open_manage_profiles).pack(side="left")
self.src_path_var = tk.StringVar()
self.dst_path_var = tk.StringVar()
self._create_path_row(path_frame, "Source (New):", self.src_path_var, 0)
@ -34,11 +110,11 @@ class MainWindow:
# 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 = 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 = 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 = ttk.Button(btn_frame, text="📥 Import Package (ZIP)", command=self._import_package)
import_btn.pack(side="left", padx=5)
# Results treeview
self._setup_treeview()
@ -47,9 +123,18 @@ class MainWindow:
"""
Creates a row with a label, entry and browse button for paths.
"""
ttk.Label(parent, text=label).grid(row=row, column=0, sticky="w", padx=5)
ttk.Entry(parent, textvariable=var, width=80).grid(row=row, column=1, padx=5, pady=2)
ttk.Button(parent, text="Browse", command=lambda: self._browse_folder(var)).grid(row=row, column=2, padx=5)
ttk.Label(parent, text=label).grid(row=row+1, column=0, sticky="w", padx=5)
entry = ttk.Entry(parent, textvariable=var, width=80)
entry.grid(row=row+1, column=1, padx=5, pady=2)
btn = ttk.Button(parent, text="📁 Browse", command=lambda: self._browse_folder(var))
btn.grid(row=row+1, column=2, padx=5)
# store references so they can be made readonly when a profile is selected
if label.lower().startswith("source"):
self.src_entry = entry
self.src_browse = btn
else:
self.dst_entry = entry
self.dst_browse = btn
def _browse_folder(self, var: tk.StringVar) -> None:
"""
@ -79,6 +164,92 @@ class MainWindow:
self.tree.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
# 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)
self.log_text.pack(fill="both", expand=True)
# Status bar at bottom
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")
# --- Profiles persistence ---
def _load_profiles(self):
try:
if os.path.exists(self._profiles_path):
with open(self._profiles_path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
pass
return {}
def _save_profiles(self):
try:
with open(self._profiles_path, "w", encoding="utf-8") as f:
json.dump(self.profiles, f, indent=2)
except Exception:
pass
def _populate_profiles(self):
names = ["(None)"] + sorted(self.profiles.keys())
self.profile_cb["values"] = names
self.profile_cb.set(names[0])
def _open_manage_profiles(self):
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()
def _close_profile_panel(self):
try:
if hasattr(self, "profile_panel") and self.profile_panel and self.profile_panel.winfo_exists():
self.profile_panel.destroy()
except Exception:
pass
# reload and refresh
self.profiles = self._load_profiles()
self._populate_profiles()
def _on_profile_change(self):
sel = self.profile_var.get()
if not sel or sel == "(None)":
# enable editing of entries
try:
self.src_entry.config(state="normal")
self.dst_entry.config(state="normal")
self.src_browse.config(state="normal")
self.dst_browse.config(state="normal")
except Exception:
pass
self.current_ignore_exts = []
return
profile = self.profiles.get(sel)
if not profile:
return
# set entries and make readonly
self.src_path_var.set(profile.get("source", ""))
self.dst_path_var.set(profile.get("destination", ""))
# load ignore extensions for comparisons
ign = profile.get("ignore_extensions", [])
if isinstance(ign, (list, tuple)):
self.current_ignore_exts = ign
elif isinstance(ign, str) and ign:
# comma-separated string
self.current_ignore_exts = [e if e.startswith('.') else '.' + e for e in (x.strip() for x in ign.split(',')) if e]
else:
self.current_ignore_exts = []
try:
self.src_entry.config(state="readonly")
self.dst_entry.config(state="readonly")
self.src_browse.config(state="disabled")
self.dst_browse.config(state="disabled")
except Exception:
pass
def _run_comparison(self) -> None:
"""
Executes comparison and populates the treeview.
@ -88,7 +259,7 @@ class MainWindow:
if not src or not dst:
messagebox.showwarning("Warning", "Please select both source and destination folders.")
return
self.comparer = CodeComparer(src, 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)
@ -154,3 +325,31 @@ class MainWindow:
commit_msg = self.sync_manager.apply_package(package_path, dst)
messagebox.showinfo("Import Complete", f"Package applied successfully.\n\nCommit Message:\n{commit_msg}")
self._run_comparison()
def _on_closing(self) -> None:
"""Clean shutdown: stop resource monitor and logging, then exit."""
try:
if self._resource_monitor:
try:
self._resource_monitor.stop()
except Exception:
pass
except Exception:
pass
try:
if self.logger_sys:
try:
self.logger_sys.shutdown()
except Exception:
pass
except Exception:
pass
try:
self.root.destroy()
except Exception:
# fallback
os._exit(0)

View File

@ -0,0 +1,223 @@
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import os
import json
from typing import Dict, Any
class ProfileManagerFrame(ttk.Frame):
"""Embeddable frame to create/edit/delete folder profiles.
Can be placed inside the main window (centered) or inside a Toplevel.
"""
def __init__(self, parent, profiles_path: str, profiles: Dict[str, Any]):
super().__init__(parent, padding=8, relief="raised")
self.parent = parent
self.profiles_path = profiles_path
self.profiles = profiles.copy()
self._setup_ui()
self._populate_list()
def _setup_ui(self):
left = ttk.Frame(self, padding=8)
left.grid(row=0, column=0, sticky="ns")
ttk.Label(left, text="Profiles").pack(anchor="w")
self.listbox = tk.Listbox(left, width=30, height=20)
self.listbox.pack(fill="y", expand=True)
self.listbox.bind("<<ListboxSelect>>", self._on_select)
right = ttk.Frame(self, padding=8)
right.grid(row=0, column=1, sticky="nsew")
# Name (required)
ttk.Label(right, text="Name (required)").grid(row=0, column=0, sticky="w")
self.name_var = tk.StringVar()
self.name_entry = ttk.Entry(right, textvariable=self.name_var)
self.name_entry.grid(row=0, column=1, sticky="ew", pady=2)
# Description (optional)
ttk.Label(right, text="Description").grid(row=1, column=0, sticky="w")
self.desc_var = tk.StringVar()
self.desc_entry = ttk.Entry(right, textvariable=self.desc_var)
self.desc_entry.grid(row=1, column=1, sticky="ew", pady=2)
# Source folder
ttk.Label(right, text="Source folder").grid(row=2, column=0, sticky="w")
self.src_var = tk.StringVar()
src_row = ttk.Frame(right)
src_row.grid(row=2, column=1, sticky="ew", pady=2)
self.src_entry = ttk.Entry(src_row, textvariable=self.src_var)
self.src_entry.pack(side="left", fill="x", expand=True)
ttk.Button(src_row, text="📁 Browse", command=self._browse_src).pack(side="left", padx=4)
# Destination folder
ttk.Label(right, text="Destination folder").grid(row=3, column=0, sticky="w")
self.dst_var = tk.StringVar()
dst_row = ttk.Frame(right)
dst_row.grid(row=3, column=1, sticky="ew", pady=2)
self.dst_entry = ttk.Entry(dst_row, textvariable=self.dst_var)
self.dst_entry.pack(side="left", fill="x", expand=True)
ttk.Button(dst_row, text="📁 Browse", command=self._browse_dst).pack(side="left", padx=4)
# Ignore extensions (multi-line text for more space)
ttk.Label(right, text="Ignore extensions (comma-separated)").grid(row=4, column=0, sticky="nw")
ignore_row = ttk.Frame(right)
ignore_row.grid(row=4, column=1, sticky="ew", pady=2)
self.ignore_text = tk.Text(ignore_row, height=3, width=60)
self.ignore_text.pack(side="left", fill="both", expand=True)
# Buttons
btn_row = ttk.Frame(right)
btn_row.grid(row=5, column=0, columnspan=2, pady=12, sticky="ew")
ttk.Button(btn_row, text=" New", command=self._new).pack(side="left", padx=4)
ttk.Button(btn_row, text="💾 Save", command=self._save).pack(side="left", padx=4)
ttk.Button(btn_row, text="🗑️ Delete", command=self._delete).pack(side="left", padx=4)
ttk.Button(btn_row, text="Insert common", command=self._insert_common_extensions).pack(side="left", padx=4)
ttk.Button(btn_row, text="✖ Close", command=self._on_close_requested).pack(side="right", padx=4)
right.columnconfigure(1, weight=1)
self.columnconfigure(1, weight=1)
# --- Internal helpers ---
def _populate_list(self):
self.listbox.delete(0, "end")
for name in sorted(self.profiles.keys()):
self.listbox.insert("end", name)
def _on_select(self, _evt=None):
sel = self.listbox.curselection()
if not sel:
return
name = self.listbox.get(sel[0])
p = self.profiles.get(name, {})
self.name_var.set(name)
self.desc_var.set(p.get("description", ""))
self.src_var.set(p.get("source", ""))
self.dst_var.set(p.get("destination", ""))
# load ignore extensions
ign = p.get("ignore_extensions")
if isinstance(ign, (list, tuple)):
self.ignore_text.delete("1.0", "end")
self.ignore_text.insert("1.0", ",".join([e.lstrip('.') for e in ign]))
else:
self.ignore_text.delete("1.0", "end")
self.ignore_text.insert("1.0", ign or "")
def _browse_src(self):
parent = self.winfo_toplevel()
d = filedialog.askdirectory(parent=parent)
if d:
self.src_var.set(d)
def _browse_dst(self):
parent = self.winfo_toplevel()
d = filedialog.askdirectory(parent=parent)
if d:
self.dst_var.set(d)
def _new(self):
self.listbox.selection_clear(0, "end")
self.name_var.set("")
self.desc_var.set("")
self.src_var.set("")
self.dst_var.set("")
self.ignore_text.delete("1.0", "end")
self.name_entry.focus()
def _save(self):
name = self.name_var.get().strip()
if not name:
messagebox.showwarning("Validation", "Profile name is required.", parent=self.winfo_toplevel())
return
profile = {
"description": self.desc_var.get().strip(),
"source": self.src_var.get().strip(),
"destination": self.dst_var.get().strip()
}
# parse ignore extensions from comma-separated input
raw = self.ignore_text.get("1.0", "end").strip()
if raw:
parts = [p.strip() for p in raw.split(",") if p.strip()]
# normalize to start with dot
norm = []
for p in parts:
if not p.startswith("."):
p = "." + p
norm.append(p)
profile["ignore_extensions"] = norm
else:
profile["ignore_extensions"] = []
self.profiles[name] = profile
try:
with open(self.profiles_path, "w", encoding="utf-8") as f:
json.dump(self.profiles, f, indent=2)
except Exception as e:
messagebox.showerror("Error", f"Failed to save profiles: {e}", parent=self.winfo_toplevel())
return
self._populate_list()
messagebox.showinfo("Saved", "Profile saved.", parent=self.winfo_toplevel())
def _delete(self):
name = self.name_var.get().strip()
if not name:
return
if name not in self.profiles:
messagebox.showwarning("Not found", "Profile not found.", parent=self.winfo_toplevel())
return
if not messagebox.askyesno("Confirm", f"Delete profile '{name}'?", parent=self.winfo_toplevel()):
return
del self.profiles[name]
try:
with open(self.profiles_path, "w", encoding="utf-8") as f:
json.dump(self.profiles, f, indent=2)
except Exception as e:
messagebox.showerror("Error", f"Failed to save profiles: {e}", parent=self.winfo_toplevel())
return
self._populate_list()
self._new()
def _insert_common_extensions(self):
# common list without dots (entry convenience)
common = ",".join([
"o", "d", "obj", "class", "pyc", "pyo", "log", "tmp", "swp", "DS_Store",
"exe", "a", "mk", "bak"
])
self.ignore_text.delete("1.0", "end")
self.ignore_text.insert("1.0", common)
def _on_close_requested(self):
# emit a virtual event so parent can handle closing/hiding
try:
self.event_generate("<<ProfileManagerClose>>")
except Exception:
pass
class ProfileManagerDialog(tk.Toplevel):
"""Backward-compatible Toplevel wrapper using the embeddable frame."""
def __init__(self, parent, profiles_path: str, profiles: Dict[str, Any]):
super().__init__(parent)
self.parent = parent
self.title("Manage Profiles")
desired_w, desired_h = 760, 420
self.geometry(f"{desired_w}x{desired_h}")
frame = ProfileManagerFrame(self, profiles_path, profiles)
frame.pack(fill="both", expand=True)
frame.bind("<<ProfileManagerClose>>", lambda e: self.destroy())
self.transient(parent)
# center over parent window
try:
self.update_idletasks()
px = parent.winfo_rootx()
py = parent.winfo_rooty()
pw = parent.winfo_width()
ph = parent.winfo_height()
x = px + max(0, (pw - desired_w) // 2)
y = py + max(0, (ph - desired_h) // 2)
self.geometry(f"{desired_w}x{desired_h}+{x}+{y}")
except Exception:
# fallback: keep default geometry
pass
self.grab_set()

47
profiles.json Normal file
View File

@ -0,0 +1,47 @@
{
"DevEnv": {
"description": "DevEnv Grifo E",
"source": "//tsclient/D/__BACKUP/GrifoE/GRIFO-E_svn/DevEnv",
"destination": "C:/src/GRIFO-E - Copia/DevEnv",
"ignore_extensions": [
".o",
".d",
".obj",
".class",
".pyc",
".pyo",
".log",
".tmp",
".swp",
".DS_Store",
".exe",
".a",
".mk",
".bak",
".defs",
".txt",
".pdom"
]
},
"REP": {
"description": "REP Grifo E code base",
"source": "C:/src/GRIFO-E - Copia/REP",
"destination": "//tsclient/D/__BACKUP/GrifoE/GRIFO-E_svn/REP",
"ignore_extensions": [
".o",
".d",
".obj",
".class",
".pyc",
".pyo",
".log",
".tmp",
".swp",
".DS_Store",
".exe",
".a",
".mk",
".bak"
]
}
}