Files
Ai/dctp/dctp_gui.py
T
Claude 8858a08a32 Add DCTP (Delta Code Transfer Protocol) tool for efficient AI code transfers
DCTP enables efficient transfer of AI-generated code using delta operations
instead of sending complete files for each modification. Features include:

- Parser for DCTP control commands (NEW, DELETE, INSERT_AFTER, REPLACE, RENUMBER)
- Line-numbered code with language-specific comment formats
- Backup/Undo system with session management
- Diff generation for preview functionality
- CustomTkinter GUI with project management, preview, and diff views
2025-12-25 12:59:07 +00:00

667 lines
21 KiB
Python

#!/usr/bin/env python3
"""
DCTP GUI - Delta Code Transfer Protocol graphical user interface.
A CustomTkinter-based GUI for managing AI-generated code transfers
using delta operations for efficient updates.
"""
import os
import sys
from datetime import datetime
from pathlib import Path
from tkinter import filedialog, messagebox
from typing import Optional
import json
import customtkinter as ctk
from dctp_parser import DCTPParser, ParseResult, Operation, OperationType
from dctp_executor import DCTPExecutor, ExecutionResult, PreviewResult, ResultStatus
from dctp_backup import BackupManager
from dctp_diff import DiffType
class SettingsDialog(ctk.CTkToplevel):
"""Settings dialog window."""
def __init__(self, parent, settings: dict):
super().__init__(parent)
self.title("Einstellungen")
self.geometry("500x400")
self.resizable(False, False)
self.settings = settings.copy()
self.result = None
# Make modal
self.transient(parent)
self.grab_set()
self._create_widgets()
# Center on parent
self.update_idletasks()
x = parent.winfo_x() + (parent.winfo_width() - self.winfo_width()) // 2
y = parent.winfo_y() + (parent.winfo_height() - self.winfo_height()) // 2
self.geometry(f"+{x}+{y}")
def _create_widgets(self):
# Main frame
main_frame = ctk.CTkFrame(self)
main_frame.pack(fill="both", expand=True, padx=20, pady=20)
# Project path
ctk.CTkLabel(main_frame, text="Standard-Projektpfad:").pack(anchor="w", pady=(0, 5))
path_frame = ctk.CTkFrame(main_frame)
path_frame.pack(fill="x", pady=(0, 15))
self.path_entry = ctk.CTkEntry(path_frame, width=350)
self.path_entry.pack(side="left", fill="x", expand=True, padx=(0, 10))
self.path_entry.insert(0, self.settings.get("project_path", ""))
ctk.CTkButton(
path_frame,
text="...",
width=40,
command=self._browse_path
).pack(side="right")
# Backup directory
ctk.CTkLabel(main_frame, text="Backup-Verzeichnis:").pack(anchor="w", pady=(0, 5))
backup_frame = ctk.CTkFrame(main_frame)
backup_frame.pack(fill="x", pady=(0, 15))
self.backup_entry = ctk.CTkEntry(backup_frame, width=350)
self.backup_entry.pack(side="left", fill="x", expand=True, padx=(0, 10))
self.backup_entry.insert(0, self.settings.get("backup_dir", ".dctp_backups"))
ctk.CTkButton(
backup_frame,
text="...",
width=40,
command=self._browse_backup
).pack(side="right")
# Options
options_frame = ctk.CTkFrame(main_frame)
options_frame.pack(fill="x", pady=15)
self.auto_renumber_var = ctk.BooleanVar(
value=self.settings.get("auto_renumber", True)
)
ctk.CTkCheckBox(
options_frame,
text="Auto-Renumber nach Operationen",
variable=self.auto_renumber_var
).pack(anchor="w", pady=5)
self.validate_checksum_var = ctk.BooleanVar(
value=self.settings.get("validate_checksum", True)
)
ctk.CTkCheckBox(
options_frame,
text="Checksum-Validierung aktiviert",
variable=self.validate_checksum_var
).pack(anchor="w", pady=5)
# Theme
ctk.CTkLabel(main_frame, text="Theme:").pack(anchor="w", pady=(15, 5))
self.theme_var = ctk.StringVar(value=self.settings.get("theme", "dark"))
theme_frame = ctk.CTkFrame(main_frame)
theme_frame.pack(fill="x", pady=(0, 15))
ctk.CTkRadioButton(
theme_frame,
text="Dunkel",
variable=self.theme_var,
value="dark"
).pack(side="left", padx=(0, 20))
ctk.CTkRadioButton(
theme_frame,
text="Hell",
variable=self.theme_var,
value="light"
).pack(side="left")
# Buttons
button_frame = ctk.CTkFrame(main_frame)
button_frame.pack(fill="x", pady=(20, 0))
ctk.CTkButton(
button_frame,
text="Abbrechen",
command=self._cancel
).pack(side="right", padx=(10, 0))
ctk.CTkButton(
button_frame,
text="Speichern",
command=self._save
).pack(side="right")
def _browse_path(self):
path = filedialog.askdirectory(
initialdir=self.path_entry.get() or os.path.expanduser("~")
)
if path:
self.path_entry.delete(0, "end")
self.path_entry.insert(0, path)
def _browse_backup(self):
path = filedialog.askdirectory(
initialdir=os.path.expanduser("~")
)
if path:
self.backup_entry.delete(0, "end")
self.backup_entry.insert(0, path)
def _save(self):
self.result = {
"project_path": self.path_entry.get(),
"backup_dir": self.backup_entry.get(),
"auto_renumber": self.auto_renumber_var.get(),
"validate_checksum": self.validate_checksum_var.get(),
"theme": self.theme_var.get()
}
self.destroy()
def _cancel(self):
self.result = None
self.destroy()
class DCTPApp(ctk.CTk):
"""Main DCTP application window."""
SETTINGS_FILE = ".dctp_settings.json"
def __init__(self):
super().__init__()
self.title("DCTP - Delta Code Transfer")
self.geometry("1200x900")
self.minsize(800, 600)
# Initialize components
self.parser = DCTPParser()
self.executor: Optional[DCTPExecutor] = None
self.backup_manager: Optional[BackupManager] = None
self.current_operations: list[Operation] = []
self.current_previews: list[PreviewResult] = []
# Load settings
self.settings = self._load_settings()
ctk.set_appearance_mode(self.settings.get("theme", "dark"))
# Create UI
self._create_widgets()
# Initialize project if path is set
if self.settings.get("project_path"):
self._init_project(self.settings["project_path"])
self._log("Bereit")
def _load_settings(self) -> dict:
"""Load settings from file."""
settings_path = Path.home() / self.SETTINGS_FILE
if settings_path.exists():
try:
return json.loads(settings_path.read_text())
except (json.JSONDecodeError, IOError):
pass
return {
"project_path": "",
"backup_dir": ".dctp_backups",
"auto_renumber": True,
"validate_checksum": True,
"theme": "dark"
}
def _save_settings(self):
"""Save settings to file."""
settings_path = Path.home() / self.SETTINGS_FILE
settings_path.write_text(json.dumps(self.settings, indent=2))
def _create_widgets(self):
"""Create all UI widgets."""
# Top bar - project selection
top_frame = ctk.CTkFrame(self)
top_frame.pack(fill="x", padx=10, pady=10)
ctk.CTkLabel(top_frame, text="Projekt:").pack(side="left", padx=(0, 10))
self.project_entry = ctk.CTkEntry(top_frame, width=400)
self.project_entry.pack(side="left", fill="x", expand=True, padx=(0, 10))
self.project_entry.insert(0, self.settings.get("project_path", ""))
ctk.CTkButton(
top_frame,
text="Waehlen",
width=100,
command=self._browse_project
).pack(side="left", padx=(0, 10))
ctk.CTkButton(
top_frame,
text="Einstellungen",
width=100,
command=self._open_settings
).pack(side="left")
# Main content area with paned layout
content_frame = ctk.CTkFrame(self)
content_frame.pack(fill="both", expand=True, padx=10, pady=(0, 10))
# Left side - Input and preview
left_frame = ctk.CTkFrame(content_frame)
left_frame.pack(side="left", fill="both", expand=True, padx=(0, 5))
# Input area
input_label_frame = ctk.CTkFrame(left_frame)
input_label_frame.pack(fill="x", pady=(5, 5), padx=5)
ctk.CTkLabel(
input_label_frame,
text="Input (KI-Output hier einfuegen)",
font=ctk.CTkFont(weight="bold")
).pack(side="left")
self.input_text = ctk.CTkTextbox(
left_frame,
height=250,
font=ctk.CTkFont(family="Consolas", size=12)
)
self.input_text.pack(fill="both", expand=True, padx=5, pady=(0, 10))
# Buttons
button_frame = ctk.CTkFrame(left_frame)
button_frame.pack(fill="x", padx=5, pady=(0, 10))
self.analyze_btn = ctk.CTkButton(
button_frame,
text="Analysieren",
command=self._analyze,
width=120
)
self.analyze_btn.pack(side="left", padx=(0, 10))
self.execute_btn = ctk.CTkButton(
button_frame,
text="Ausfuehren",
command=self._execute,
width=120,
state="disabled"
)
self.execute_btn.pack(side="left", padx=(0, 10))
self.undo_btn = ctk.CTkButton(
button_frame,
text="Undo",
command=self._undo,
width=80
)
self.undo_btn.pack(side="left", padx=(0, 10))
ctk.CTkButton(
button_frame,
text="Clear",
command=self._clear,
width=80
).pack(side="left")
# Preview operations
preview_label_frame = ctk.CTkFrame(left_frame)
preview_label_frame.pack(fill="x", pady=(5, 5), padx=5)
ctk.CTkLabel(
preview_label_frame,
text="Vorschau Operationen",
font=ctk.CTkFont(weight="bold")
).pack(side="left")
self.preview_text = ctk.CTkTextbox(
left_frame,
height=150,
font=ctk.CTkFont(family="Consolas", size=11)
)
self.preview_text.pack(fill="both", expand=True, padx=5, pady=(0, 10))
self.preview_text.configure(state="disabled")
# Right side - Diff and file tree
right_frame = ctk.CTkFrame(content_frame)
right_frame.pack(side="right", fill="both", expand=True, padx=(5, 0))
# Diff view
diff_label_frame = ctk.CTkFrame(right_frame)
diff_label_frame.pack(fill="x", pady=(5, 5), padx=5)
ctk.CTkLabel(
diff_label_frame,
text="Diff-Ansicht",
font=ctk.CTkFont(weight="bold")
).pack(side="left")
self.diff_text = ctk.CTkTextbox(
right_frame,
height=300,
font=ctk.CTkFont(family="Consolas", size=11)
)
self.diff_text.pack(fill="both", expand=True, padx=5, pady=(0, 10))
self.diff_text.configure(state="disabled")
# File tree
tree_label_frame = ctk.CTkFrame(right_frame)
tree_label_frame.pack(fill="x", pady=(5, 5), padx=5)
ctk.CTkLabel(
tree_label_frame,
text="Projektdateien",
font=ctk.CTkFont(weight="bold")
).pack(side="left")
ctk.CTkButton(
tree_label_frame,
text="Aktualisieren",
width=80,
command=self._refresh_file_tree
).pack(side="right")
self.tree_text = ctk.CTkTextbox(
right_frame,
height=150,
font=ctk.CTkFont(family="Consolas", size=11)
)
self.tree_text.pack(fill="both", expand=True, padx=5, pady=(0, 10))
self.tree_text.configure(state="disabled")
# Bottom - Log
log_label_frame = ctk.CTkFrame(self)
log_label_frame.pack(fill="x", padx=10, pady=(0, 5))
ctk.CTkLabel(
log_label_frame,
text="Log",
font=ctk.CTkFont(weight="bold")
).pack(side="left")
self.log_text = ctk.CTkTextbox(
self,
height=120,
font=ctk.CTkFont(family="Consolas", size=10)
)
self.log_text.pack(fill="x", padx=10, pady=(0, 10))
self.log_text.configure(state="disabled")
def _log(self, message: str, level: str = "info"):
"""Add a message to the log."""
timestamp = datetime.now().strftime("%H:%M:%S")
prefix = ""
if level == "error":
prefix = "ERROR "
elif level == "warning":
prefix = "WARN "
self.log_text.configure(state="normal")
self.log_text.insert("end", f"{timestamp} {prefix}{message}\n")
self.log_text.see("end")
self.log_text.configure(state="disabled")
def _init_project(self, path: str):
"""Initialize project with given path."""
if not os.path.isdir(path):
self._log(f"Verzeichnis existiert nicht: {path}", "error")
return False
self.backup_manager = BackupManager(path)
self.executor = DCTPExecutor(
path,
self.backup_manager,
auto_renumber=self.settings.get("auto_renumber", True),
validate_checksums=self.settings.get("validate_checksum", True)
)
self.settings["project_path"] = path
self._save_settings()
self._log(f"Projekt geladen: {path}")
self._refresh_file_tree()
return True
def _browse_project(self):
"""Open directory browser for project selection."""
initial_dir = self.project_entry.get() or os.path.expanduser("~")
path = filedialog.askdirectory(initialdir=initial_dir)
if path:
self.project_entry.delete(0, "end")
self.project_entry.insert(0, path)
self._init_project(path)
def _open_settings(self):
"""Open settings dialog."""
dialog = SettingsDialog(self, self.settings)
self.wait_window(dialog)
if dialog.result:
old_theme = self.settings.get("theme")
self.settings.update(dialog.result)
self._save_settings()
# Apply theme change
if dialog.result.get("theme") != old_theme:
ctk.set_appearance_mode(dialog.result["theme"])
# Reinitialize project with new settings
if self.settings.get("project_path"):
self._init_project(self.settings["project_path"])
self._log("Einstellungen gespeichert")
def _analyze(self):
"""Analyze input and show preview."""
# Ensure project is initialized
project_path = self.project_entry.get()
if not project_path:
messagebox.showerror("Fehler", "Bitte waehle ein Projektverzeichnis")
return
if not self.executor or self.settings.get("project_path") != project_path:
if not self._init_project(project_path):
return
# Get input
input_text = self.input_text.get("1.0", "end-1c")
if not input_text.strip():
self._log("Kein Input vorhanden", "warning")
return
# Parse
self._log("Analysiere...")
result = self.parser.parse(input_text)
# Handle errors
if result.has_errors:
self._log(f"{len(result.errors)} Parse-Fehler gefunden", "error")
for error in result.errors:
self._log(f" Zeile {error.line_number}: {error.message}", "error")
return
if not result.operations:
self._log("Keine Operationen gefunden", "warning")
return
self.current_operations = result.operations
self._log(f"{len(result.operations)} Operationen gefunden")
# Generate previews
self.current_previews = self.executor.preview(result.operations)
# Display previews
self._display_previews()
# Enable execute button
self.execute_btn.configure(state="normal")
def _display_previews(self):
"""Display operation previews."""
self.preview_text.configure(state="normal")
self.preview_text.delete("1.0", "end")
self.diff_text.configure(state="normal")
self.diff_text.delete("1.0", "end")
for preview in self.current_previews:
# Add to preview list
self.preview_text.insert("end", f"{preview.description}\n")
for warning in preview.warnings:
self.preview_text.insert("end", f" WARNING {warning}\n")
# Add diff if available
if preview.diff and preview.diff.has_changes:
self.diff_text.insert("end", f"--- {preview.diff.filename} ---\n")
for line in preview.diff.lines:
if line.type == DiffType.ADDED:
self.diff_text.insert("end", f"+ {line.content}\n")
elif line.type == DiffType.REMOVED:
self.diff_text.insert("end", f"- {line.content}\n")
elif line.type == DiffType.UNCHANGED:
self.diff_text.insert("end", f" {line.content}\n")
self.diff_text.insert("end", "\n")
self.preview_text.configure(state="disabled")
self.diff_text.configure(state="disabled")
def _execute(self):
"""Execute the analyzed operations."""
if not self.current_operations:
self._log("Keine Operationen zum Ausfuehren", "warning")
return
if not self.executor:
self._log("Kein Projekt initialisiert", "error")
return
# Confirm
count = len(self.current_operations)
if not messagebox.askyesno(
"Bestaetigen",
f"{count} Operationen ausfuehren?"
):
return
self._log(f"Fuehre {count} Operationen aus...")
# Execute
results = self.executor.execute(self.current_operations)
# Log results
success_count = 0
for result in results:
if result.status == ResultStatus.SUCCESS:
self._log(f"OK {result.message}")
success_count += 1
elif result.status == ResultStatus.WARNING:
self._log(f"WARN {result.message}", "warning")
elif result.status == ResultStatus.ERROR:
self._log(f"ERROR {result.message}", "error")
self._log(f"Abgeschlossen: {success_count}/{count} erfolgreich")
# Clear current operations
self.current_operations = []
self.current_previews = []
self.execute_btn.configure(state="disabled")
# Refresh file tree
self._refresh_file_tree()
def _undo(self):
"""Undo last operation."""
if not self.backup_manager:
self._log("Kein Projekt initialisiert", "error")
return
success, restored = self.backup_manager.restore_last()
if success:
self._log(f"Undo erfolgreich: {len(restored)} Dateien wiederhergestellt")
for f in restored:
self._log(f" -> {f}")
self._refresh_file_tree()
else:
self._log("Kein Backup zum Wiederherstellen", "warning")
def _clear(self):
"""Clear input and preview areas."""
self.input_text.delete("1.0", "end")
self.preview_text.configure(state="normal")
self.preview_text.delete("1.0", "end")
self.preview_text.configure(state="disabled")
self.diff_text.configure(state="normal")
self.diff_text.delete("1.0", "end")
self.diff_text.configure(state="disabled")
self.current_operations = []
self.current_previews = []
self.execute_btn.configure(state="disabled")
self._log("Eingabe geloescht")
def _refresh_file_tree(self):
"""Refresh the file tree display."""
self.tree_text.configure(state="normal")
self.tree_text.delete("1.0", "end")
project_path = self.project_entry.get()
if not project_path or not os.path.isdir(project_path):
self.tree_text.insert("end", "(Kein Projekt geladen)")
self.tree_text.configure(state="disabled")
return
# Build simple tree
try:
self._add_tree_items(Path(project_path), 0)
except Exception as e:
self.tree_text.insert("end", f"Fehler: {e}")
self.tree_text.configure(state="disabled")
def _add_tree_items(self, path: Path, level: int, max_items: int = 100):
"""Recursively add items to tree display."""
if level > 5: # Limit depth
return
indent = " " * level
try:
items = sorted(path.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower()))
count = 0
for item in items:
if count >= max_items:
self.tree_text.insert("end", f"{indent} ... (mehr Dateien)\n")
break
# Skip hidden files and backup directory
if item.name.startswith('.'):
continue
if item.is_dir():
self.tree_text.insert("end", f"{indent}DIR {item.name}/\n")
self._add_tree_items(item, level + 1, max_items=20)
else:
self.tree_text.insert("end", f"{indent}FILE {item.name}\n")
count += 1
except PermissionError:
self.tree_text.insert("end", f"{indent} (Zugriff verweigert)\n")
def main():
"""Main entry point."""
app = DCTPApp()
app.mainloop()
if __name__ == "__main__":
main()