218 lines
8.9 KiB
Python
218 lines
8.9 KiB
Python
import tkinter as tk
|
|
from tkinter import ttk, messagebox, filedialog
|
|
from pathlib import Path
|
|
from typing import List
|
|
|
|
from ..config import profiles as profiles_cfg
|
|
from ..config.languages import LANGUAGE_EXTENSIONS
|
|
|
|
|
|
class ProfileManager(tk.Toplevel):
|
|
"""Dialog window to create/edit/delete scanning profiles.
|
|
|
|
Usage: pm = ProfileManager(root); pm.wait_window()
|
|
"""
|
|
|
|
COMMON_LANGS = ["Python", "C", "C++", "Java", "JavaScript", "HTML", "Shell"]
|
|
# sensible defaults for ignore patterns covering common build/artifact dirs and files
|
|
DEFAULT_IGNORES = [
|
|
"__pycache__",
|
|
"*.pyc",
|
|
"*.pyo",
|
|
"*.pyd",
|
|
".Python",
|
|
"env",
|
|
"venv",
|
|
".venv",
|
|
"build",
|
|
"dist",
|
|
"*.egg-info",
|
|
".eggs",
|
|
"node_modules",
|
|
".git",
|
|
".hg",
|
|
".svn",
|
|
".idea",
|
|
".vscode",
|
|
".DS_Store",
|
|
"*.class",
|
|
"*.o",
|
|
"*.so",
|
|
"*.dylib",
|
|
".pytest_cache",
|
|
".mypy_cache",
|
|
".cache",
|
|
"coverage",
|
|
".tox",
|
|
"pip-wheel-metadata",
|
|
]
|
|
|
|
def __init__(self, parent, on_change=None):
|
|
super().__init__(parent)
|
|
self.title("Profile Manager")
|
|
self.geometry("900x600")
|
|
self.on_change = on_change
|
|
|
|
self.profiles = profiles_cfg.load_profiles()
|
|
|
|
# Left: list of profiles
|
|
left = ttk.Frame(self)
|
|
left.pack(side="left", fill="y", padx=8, pady=8)
|
|
# Make listbox taller so names are easily visible
|
|
self.listbox = tk.Listbox(left, width=30, height=20)
|
|
self.listbox.pack(fill="y", expand=True)
|
|
for p in self.profiles:
|
|
self.listbox.insert("end", p.get("name"))
|
|
self.listbox.bind("<<ListboxSelect>>", self._on_select)
|
|
|
|
# Right: edit form
|
|
right = ttk.Frame(self)
|
|
right.pack(side="left", fill="both", expand=True, padx=8, pady=8)
|
|
|
|
ttk.Label(right, text="Profile name:").grid(row=0, column=0, sticky="w", pady=4)
|
|
self.name_var = tk.StringVar()
|
|
ttk.Entry(right, textvariable=self.name_var, width=48).grid(row=0, column=1, columnspan=2, sticky="ew", pady=4)
|
|
|
|
# Path: label above large entry spanning full right area
|
|
ttk.Label(right, text="Path:").grid(row=1, column=0, sticky="w", pady=(8, 2))
|
|
self.path_var = tk.StringVar()
|
|
ttk.Entry(right, textvariable=self.path_var, width=80).grid(row=2, column=0, columnspan=2, sticky="ew", pady=2)
|
|
ttk.Button(right, text="Browse...", command=self._browse_path).grid(row=2, column=2, sticky="w", padx=4)
|
|
|
|
ttk.Label(right, text="Languages:").grid(row=3, column=0, sticky="nw", pady=(8, 2))
|
|
# Scrollable frame for languages so checkboxes remain readable
|
|
langs_container = ttk.Frame(right)
|
|
langs_container.grid(row=3, column=1, columnspan=2, sticky="ew", pady=4)
|
|
canvas = tk.Canvas(langs_container, height=140)
|
|
vsb = ttk.Scrollbar(langs_container, orient="vertical", command=canvas.yview)
|
|
inner = ttk.Frame(canvas)
|
|
inner.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
|
|
canvas.create_window((0, 0), window=inner, anchor="nw")
|
|
canvas.configure(yscrollcommand=vsb.set)
|
|
canvas.grid(row=0, column=0, sticky="nsew")
|
|
vsb.grid(row=0, column=1, sticky="ns")
|
|
langs_container.columnconfigure(0, weight=1)
|
|
|
|
self.lang_vars = {}
|
|
for i, ln in enumerate(self.COMMON_LANGS):
|
|
var = tk.BooleanVar()
|
|
exts = LANGUAGE_EXTENSIONS.get(ln, [])
|
|
exts_label = f" ({', '.join(exts)})" if exts else ""
|
|
cb = ttk.Checkbutton(inner, text=ln + exts_label, variable=var)
|
|
cb.grid(row=i, column=0, sticky="w", padx=2, pady=2)
|
|
self.lang_vars[ln] = var
|
|
|
|
# Custom languages: label above large text box spanning full width
|
|
ttk.Label(right, text="Custom languages (comma-separated):").grid(row=4, column=0, sticky="w", pady=(8, 2))
|
|
self.custom_text = tk.Text(right, height=3, wrap="none")
|
|
self.custom_text.grid(row=5, column=0, columnspan=3, sticky="nsew", pady=2)
|
|
|
|
# Ignore patterns: label above large text box with scrollbars
|
|
ttk.Label(right, text="Ignore patterns (comma-separated):").grid(row=6, column=0, sticky="w", pady=(8, 2))
|
|
ignore_container = ttk.Frame(right)
|
|
ignore_container.grid(row=7, column=0, columnspan=3, sticky="nsew", pady=2)
|
|
# Text widget with both vertical and horizontal scrollbars so long patterns are readable
|
|
self.ignore_text = tk.Text(ignore_container, height=4, wrap="none")
|
|
self.ignore_text.grid(row=0, column=0, sticky="nsew")
|
|
vsb_ignore = ttk.Scrollbar(ignore_container, orient="vertical", command=self.ignore_text.yview)
|
|
vsb_ignore.grid(row=0, column=1, sticky="ns")
|
|
hsb_ignore = ttk.Scrollbar(ignore_container, orient="horizontal", command=self.ignore_text.xview)
|
|
hsb_ignore.grid(row=1, column=0, columnspan=2, sticky="ew")
|
|
self.ignore_text.configure(yscrollcommand=vsb_ignore.set, xscrollcommand=hsb_ignore.set)
|
|
ignore_container.columnconfigure(0, weight=1)
|
|
ignore_container.rowconfigure(0, weight=1)
|
|
|
|
# Buttons (place below the large edit boxes)
|
|
btn_frame = ttk.Frame(right)
|
|
btn_frame.grid(row=8, column=0, columnspan=3, pady=(12, 0))
|
|
ttk.Button(btn_frame, text="New", command=self._new).grid(row=0, column=0, padx=4)
|
|
ttk.Button(btn_frame, text="Save", command=self._save).grid(row=0, column=1, padx=4)
|
|
ttk.Button(btn_frame, text="Delete", command=self._delete).grid(row=0, column=2, padx=4)
|
|
ttk.Button(btn_frame, text="Close", command=self.destroy).grid(row=0, column=3, padx=4)
|
|
|
|
right.columnconfigure(1, weight=1)
|
|
|
|
# If no profiles exist, prefill form with sensible defaults (widgets are ready)
|
|
if not self.profiles:
|
|
self._new()
|
|
|
|
def _browse_path(self):
|
|
d = filedialog.askdirectory()
|
|
if d:
|
|
self.path_var.set(str(Path(d)))
|
|
|
|
def _on_select(self, _evt=None):
|
|
sel = self.listbox.curselection()
|
|
if not sel:
|
|
return
|
|
idx = sel[0]
|
|
pr = self.profiles[idx]
|
|
self._load_profile(pr)
|
|
|
|
def _load_profile(self, pr):
|
|
self.name_var.set(pr.get("name", ""))
|
|
self.path_var.set(pr.get("path", ""))
|
|
langs = pr.get("languages", []) or []
|
|
for ln, var in self.lang_vars.items():
|
|
var.set(ln in langs)
|
|
custom = ",".join([l for l in langs if l not in self.COMMON_LANGS])
|
|
# populate custom_text (large edit box)
|
|
self.custom_text.delete("1.0", "end")
|
|
if custom:
|
|
self.custom_text.insert("1.0", custom)
|
|
# populate ignore_text
|
|
self.ignore_text.delete("1.0", "end")
|
|
ignores = ",".join(pr.get("ignore", []))
|
|
if ignores:
|
|
self.ignore_text.insert("1.0", ignores)
|
|
|
|
def _new(self):
|
|
self.name_var.set("")
|
|
self.path_var.set("")
|
|
for var in self.lang_vars.values():
|
|
var.set(False)
|
|
self.custom_text.delete("1.0", "end")
|
|
self.ignore_text.delete("1.0", "end")
|
|
# insert the DEFAULT_IGNORES joined by commas
|
|
self.ignore_text.insert("1.0", ",".join(self.DEFAULT_IGNORES))
|
|
|
|
def _save(self):
|
|
name = self.name_var.get().strip()
|
|
if not name:
|
|
messagebox.showwarning("Validation", "Profile must have a name.")
|
|
return
|
|
path = self.path_var.get().strip()
|
|
langs: List[str] = [ln for ln, var in self.lang_vars.items() if var.get()]
|
|
raw_custom = self.custom_text.get("1.0", "end").strip()
|
|
raw_custom = raw_custom.replace("\n", ",")
|
|
custom = [c.strip() for c in raw_custom.split(",") if c.strip()]
|
|
langs.extend([c for c in custom if c])
|
|
raw_ignore = self.ignore_text.get("1.0", "end").strip()
|
|
raw_ignore = raw_ignore.replace("\n", ",")
|
|
ignore = [s.strip() for s in raw_ignore.split(",") if s.strip()]
|
|
profile = {"name": name, "path": path, "languages": langs, "ignore": ignore}
|
|
profiles_cfg.add_or_update_profile(profile)
|
|
self.profiles = profiles_cfg.load_profiles()
|
|
# refresh listbox
|
|
self.listbox.delete(0, "end")
|
|
for p in self.profiles:
|
|
self.listbox.insert("end", p.get("name"))
|
|
messagebox.showinfo("Saved", f"Profile '{name}' saved.")
|
|
if self.on_change:
|
|
self.on_change()
|
|
|
|
def _delete(self):
|
|
name = self.name_var.get().strip()
|
|
if not name:
|
|
return
|
|
if not messagebox.askyesno("Delete", f"Delete profile '{name}'?"):
|
|
return
|
|
profiles_cfg.delete_profile(name)
|
|
self.profiles = profiles_cfg.load_profiles()
|
|
self.listbox.delete(0, "end")
|
|
for p in self.profiles:
|
|
self.listbox.insert("end", p.get("name"))
|
|
self._new()
|
|
if self.on_change:
|
|
self.on_change()
|