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
This commit is contained in:
Claude
2025-12-25 12:59:07 +00:00
parent faa36d0e5e
commit 8858a08a32
9 changed files with 2502 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
__pycache__/
*.pyc
*.pyo
.dctp_backups/
.dctp_settings.json
+102
View File
@@ -0,0 +1,102 @@
# DCTP - Delta Code Transfer Protocol
Du generierst Code im DCTP-Format fuer effiziente Uebertragung.
## Regeln
1. **Zeilennummern am Ende jeder Zeile** im passenden Kommentar-Format
2. **Immer mit ###FILE: beginnen** bei jedem Codeblock
3. **Bei Korrekturen NUR die geaenderten Zeilen senden**, nie den ganzen File
## Zeilennummern-Format
- Python/Shell: `code #Z1`
- JavaScript/Java/C/C++: `code //Z1`
- HTML: `code <!--Z1-->`
- CSS: `code /*Z1*/`
- SQL: `code --Z1`
## Befehle
| Befehl | Syntax | Beschreibung |
|--------|--------|--------------|
| `###FILE:` | `###FILE:pfad/datei.ext` | Datei angeben |
| `###NEW` | | Neue Datei, kompletter Inhalt folgt |
| `###DELETE:` | `###DELETE:Z5-Z12` | Zeilen 5-12 loeschen |
| `###INSERT_AFTER:` | `###INSERT_AFTER:Z5` | Nach Zeile 5 einfuegen |
| `###REPLACE:` | `###REPLACE:Z5-Z8` | Zeilen 5-8 ersetzen |
| `###END` | | Ende des Blocks |
| `###RENUMBER` | | Zeilennummern neu berechnen |
| `###CHECKSUM:` | `###CHECKSUM:a3f2b8c1` | Optional: Hash zur Validierung |
## Beispiel: Neue Datei
```
###FILE:src/calculator.py
###NEW
def add(a, b): #Z1
return a + b #Z2
#Z3
def multiply(a, b): #Z4
return a * b #Z5
###END
```
## Beispiel: Korrektur (REPLACE)
```
###FILE:src/calculator.py
###REPLACE:Z4-Z5
def multiply(a, b): #Z4
"""Multipliziert zwei Zahlen.""" #Z5
return a * b #Z6
###END
###RENUMBER
```
## Beispiel: Zeilen einfuegen
```
###FILE:src/calculator.py
###INSERT_AFTER:Z2
#Z3
def subtract(a, b): #Z4
return a - b #Z5
###END
###RENUMBER
```
## Beispiel: Zeilen loeschen
```
###FILE:src/calculator.py
###DELETE:Z10-Z15
###RENUMBER
```
## Beispiel: Mehrere Dateien
```
###FILE:src/models/user.py
###NEW
class User: #Z1
def __init__(self, name: str): #Z2
self.name = name #Z3
###END
###FILE:src/models/order.py
###NEW
from .user import User #Z1
#Z2
class Order: #Z3
def __init__(self, user: User): #Z4
self.user = user #Z5
###END
```
## Wichtig
- Bei Korrekturen: NUR Delta senden, nie kompletten File
- Nach INSERT/DELETE/REPLACE immer ###RENUMBER
- Leerzeilen auch nummerieren
- Zeilennummern werden beim Schreiben automatisch entfernt
+147
View File
@@ -0,0 +1,147 @@
# DCTP - Delta Code Transfer Protocol
Ein Tool um KI-generierten Code effizient in lokale Dateien zu uebertragen. Statt bei jeder Korrektur den kompletten Code neu zu senden, werden nur Aenderungen (Deltas) uebertragen.
## Das Problem
Claude generiert 500 Zeilen Code. Eine kleine Korrektur = nochmal 500 Zeilen. Verschwendung.
## Die Loesung
Zeilennummerierter Code + Steueranweisungen fuer gezielte Aenderungen.
## Installation
```bash
# Requirements installieren
pip install -r requirements.txt
# GUI starten
python dctp_gui.py
```
## Schnellstart
### 1. Projektverzeichnis waehlen
Klicke auf "Waehlen" und waehle dein Projektverzeichnis.
### 2. KI-Output einfuegen
Kopiere den DCTP-formatierten Output aus deinem Claude-Chat in das Input-Feld.
### 3. Analysieren
Klicke "Analysieren" um eine Vorschau der Operationen zu sehen.
### 4. Ausfuehren
Klicke "Ausfuehren" um die Aenderungen auf deine Dateien anzuwenden.
## DCTP-Format
### Neue Datei erstellen
```
###FILE:src/calculator.py
###NEW
def add(a, b): #Z1
return a + b #Z2
###END
```
### Zeilen ersetzen
```
###FILE:src/calculator.py
###REPLACE:Z1-Z2
def add(a: int, b: int) -> int: #Z1
"""Addiert zwei Zahlen.""" #Z2
return a + b #Z3
###END
###RENUMBER
```
### Zeilen einfuegen
```
###FILE:src/calculator.py
###INSERT_AFTER:Z2
#Z3
def subtract(a, b): #Z4
return a - b #Z5
###END
###RENUMBER
```
### Zeilen loeschen
```
###FILE:src/calculator.py
###DELETE:Z10-Z15
###RENUMBER
```
## Zeilennummern-Format
Die Zeilennummern werden automatisch entsprechend der Programmiersprache formatiert:
| Sprache | Format | Beispiel |
|---------|--------|----------|
| Python | `#Z1` | `code #Z1` |
| JavaScript | `//Z1` | `code //Z1` |
| HTML | `<!--Z1-->` | `code <!--Z1-->` |
| CSS | `/*Z1*/` | `code /*Z1*/` |
| SQL | `--Z1` | `code --Z1` |
## Befehle
| Befehl | Beschreibung |
|--------|--------------|
| `###FILE:pfad` | Zieldatei angeben |
| `###NEW` | Neue Datei erstellen |
| `###DELETE:Z5-Z12` | Zeilen loeschen |
| `###INSERT_AFTER:Z5` | Nach Zeile einfuegen |
| `###REPLACE:Z5-Z8` | Zeilen ersetzen |
| `###END` | Block beenden |
| `###RENUMBER` | Zeilennummern aktualisieren |
| `###CHECKSUM:hash` | Datei-Hash validieren |
## Features
- **Vorschau**: Zeigt was passieren wird, bevor es ausgefuehrt wird
- **Diff-Ansicht**: Zeigt Aenderungen farbig markiert (alt vs neu)
- **Undo**: Stellt den letzten Zustand wieder her
- **Backup**: Automatische Backups vor jeder Aenderung
- **Multi-File**: Mehrere Dateien in einem Durchgang bearbeiten
- **Checksum**: Optionale Validierung gegen externe Aenderungen
## Einstellungen
- **Projektpfad**: Standard-Projektverzeichnis
- **Backup-Verzeichnis**: Wo Backups gespeichert werden
- **Auto-Renumber**: Zeilennummern automatisch aktualisieren
- **Checksum-Validierung**: Externe Aenderungen erkennen
- **Theme**: Hell oder dunkel
## Claude-Integration
Kopiere den Inhalt von `CLAUDE.md` in deine Claude-Chats (als Custom Instructions oder am Anfang des Gespraechs), damit Claude im DCTP-Format antwortet.
## Architektur
```
dctp/
├── dctp_gui.py # Hauptfenster (CustomTkinter)
├── dctp_parser.py # Core-Logik: parse Steueranweisungen
├── dctp_executor.py # Fuehrt Operationen aus
├── dctp_backup.py # Undo/Backup-Verwaltung
├── dctp_diff.py # Diff-Berechnung fuer Vorschau
├── requirements.txt # Dependencies
├── CLAUDE.md # Anweisung fuer KI
└── README.md # Diese Datei
```
## Lizenz
MIT License
+305
View File
@@ -0,0 +1,305 @@
"""
DCTP Backup Manager - Handles backup and undo functionality.
Creates timestamped backups before operations and supports
restoring files to their previous state.
"""
import json
import os
import shutil
from dataclasses import dataclass, asdict
from datetime import datetime
from pathlib import Path
from typing import Optional
@dataclass
class FileBackup:
"""Represents a single file backup."""
original: str
backup: str
existed: bool # Whether the file existed before (for new file handling)
@dataclass
class BackupSession:
"""Represents a backup session (one execution run)."""
timestamp: str
files: list[FileBackup]
@dataclass
class BackupInfo:
"""Info about a backup for display purposes."""
timestamp: str
file_count: int
files: list[str]
class BackupManager:
"""Manages file backups for undo functionality."""
BACKUP_DIR_NAME = ".dctp_backups"
MANIFEST_FILE = "manifest.json"
MAX_SESSIONS = 50 # Keep last 50 sessions
def __init__(self, project_path: str):
"""
Initialize backup manager.
Args:
project_path: Base project directory
"""
self.project_path = Path(project_path)
self.backup_dir = self.project_path / self.BACKUP_DIR_NAME
self.manifest_path = self.backup_dir / self.MANIFEST_FILE
self._current_session: Optional[BackupSession] = None
self._ensure_backup_dir()
def _ensure_backup_dir(self) -> None:
"""Create backup directory if it doesn't exist."""
self.backup_dir.mkdir(parents=True, exist_ok=True)
# Create .gitignore in backup dir
gitignore_path = self.backup_dir / ".gitignore"
if not gitignore_path.exists():
gitignore_path.write_text("*\n")
def _load_manifest(self) -> dict:
"""Load the manifest file."""
if self.manifest_path.exists():
try:
return json.loads(self.manifest_path.read_text())
except (json.JSONDecodeError, IOError):
return {"sessions": []}
return {"sessions": []}
def _save_manifest(self, manifest: dict) -> None:
"""Save the manifest file."""
self.manifest_path.write_text(json.dumps(manifest, indent=2))
def start_session(self) -> None:
"""Start a new backup session."""
timestamp = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
self._current_session = BackupSession(timestamp=timestamp, files=[])
def backup(self, file_path: str) -> Optional[str]:
"""
Create a backup of a file.
Args:
file_path: Path to the file (relative to project or absolute)
Returns:
Backup filename if successful, None if file doesn't exist
"""
if self._current_session is None:
self.start_session()
# Normalize path
if os.path.isabs(file_path):
full_path = Path(file_path)
rel_path = full_path.relative_to(self.project_path)
else:
rel_path = Path(file_path)
full_path = self.project_path / rel_path
# Check if file exists
existed = full_path.exists()
if existed:
# Generate backup filename
timestamp = datetime.now().strftime("%Y-%m-%d_%H%M%S")
safe_name = str(rel_path).replace(os.sep, "_").replace("/", "_")
backup_name = f"{timestamp}_{safe_name}"
# Copy file to backup
backup_path = self.backup_dir / backup_name
shutil.copy2(full_path, backup_path)
else:
backup_name = ""
# Add to current session
self._current_session.files.append(FileBackup(
original=str(rel_path),
backup=backup_name,
existed=existed
))
return backup_name if existed else None
def end_session(self) -> None:
"""End the current backup session and save to manifest."""
if self._current_session is None or len(self._current_session.files) == 0:
self._current_session = None
return
manifest = self._load_manifest()
# Convert to dict for JSON storage
session_dict = {
"timestamp": self._current_session.timestamp,
"files": [asdict(f) for f in self._current_session.files]
}
manifest["sessions"].append(session_dict)
# Limit number of sessions
if len(manifest["sessions"]) > self.MAX_SESSIONS:
# Remove old sessions and their backup files
old_sessions = manifest["sessions"][:-self.MAX_SESSIONS]
for session in old_sessions:
for file_info in session["files"]:
backup_file = self.backup_dir / file_info["backup"]
if backup_file.exists():
backup_file.unlink()
manifest["sessions"] = manifest["sessions"][-self.MAX_SESSIONS:]
self._save_manifest(manifest)
self._current_session = None
def restore_last(self) -> tuple[bool, list[str]]:
"""
Restore files from the last backup session.
Returns:
Tuple of (success, list of restored files)
"""
manifest = self._load_manifest()
if not manifest["sessions"]:
return False, []
# Get last session
last_session = manifest["sessions"].pop()
restored_files = []
for file_info in last_session["files"]:
original_path = self.project_path / file_info["original"]
if file_info["existed"]:
# Restore from backup
backup_path = self.backup_dir / file_info["backup"]
if backup_path.exists():
# Ensure parent directory exists
original_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(backup_path, original_path)
backup_path.unlink() # Remove backup file
restored_files.append(file_info["original"])
else:
# File was newly created, delete it
if original_path.exists():
original_path.unlink()
restored_files.append(f"{file_info['original']} (deleted)")
self._save_manifest(manifest)
return True, restored_files
def list_backups(self) -> list[BackupInfo]:
"""
List all backup sessions.
Returns:
List of BackupInfo objects, newest first
"""
manifest = self._load_manifest()
backups = []
for session in reversed(manifest["sessions"]):
files = [f["original"] for f in session["files"]]
backups.append(BackupInfo(
timestamp=session["timestamp"],
file_count=len(files),
files=files
))
return backups
def get_file_backup_path(self, file_path: str) -> Optional[Path]:
"""
Get the backup path for a file from the most recent session.
Args:
file_path: Original file path (relative to project)
Returns:
Path to backup file if found, None otherwise
"""
manifest = self._load_manifest()
if not manifest["sessions"]:
return None
# Search from newest to oldest
for session in reversed(manifest["sessions"]):
for file_info in session["files"]:
if file_info["original"] == file_path and file_info["existed"]:
backup_path = self.backup_dir / file_info["backup"]
if backup_path.exists():
return backup_path
return None
def clear_all_backups(self) -> int:
"""
Clear all backups.
Returns:
Number of backup files deleted
"""
count = 0
if self.backup_dir.exists():
for item in self.backup_dir.iterdir():
if item.name != ".gitignore":
if item.is_file():
item.unlink()
count += 1
elif item.is_dir():
shutil.rmtree(item)
count += 1
# Reset manifest
self._save_manifest({"sessions": []})
return count
def main():
"""Test the backup manager."""
import tempfile
# Create a temporary project directory
with tempfile.TemporaryDirectory() as tmpdir:
# Create some test files
test_file = Path(tmpdir) / "test.py"
test_file.write_text("print('hello')\n")
# Initialize backup manager
manager = BackupManager(tmpdir)
# Start a session and backup the file
manager.start_session()
backup_name = manager.backup("test.py")
print(f"Created backup: {backup_name}")
# Modify the file
test_file.write_text("print('modified')\n")
print(f"File content after modification: {test_file.read_text()}")
# End session
manager.end_session()
# List backups
backups = manager.list_backups()
print(f"Backup sessions: {len(backups)}")
for b in backups:
print(f" {b.timestamp}: {b.file_count} files")
# Restore
success, restored = manager.restore_last()
print(f"Restore successful: {success}")
print(f"Restored files: {restored}")
print(f"File content after restore: {test_file.read_text()}")
if __name__ == "__main__":
main()
+385
View File
@@ -0,0 +1,385 @@
"""
DCTP Diff Generator - Generates diffs for preview display.
Compares old and new content and produces colored diff output
for the GUI preview.
"""
import difflib
from dataclasses import dataclass
from enum import Enum
from typing import Optional
class DiffType(Enum):
UNCHANGED = "unchanged"
ADDED = "added"
REMOVED = "removed"
CONTEXT = "context"
@dataclass
class DiffLine:
"""Represents a single line in a diff."""
type: DiffType
line_number_old: Optional[int] # Line number in old file
line_number_new: Optional[int] # Line number in new file
content: str
@property
def prefix(self) -> str:
"""Get the diff prefix character."""
if self.type == DiffType.ADDED:
return "+"
elif self.type == DiffType.REMOVED:
return "-"
else:
return " "
def __str__(self) -> str:
old_num = str(self.line_number_old) if self.line_number_old else ""
new_num = str(self.line_number_new) if self.line_number_new else ""
return f"{old_num:>4} {new_num:>4} {self.prefix} {self.content}"
@dataclass
class DiffBlock:
"""A block of related diff lines."""
start_old: int
end_old: int
start_new: int
end_new: int
lines: list[DiffLine]
@property
def header(self) -> str:
"""Generate a unified diff style header."""
return f"@@ -{self.start_old},{self.end_old - self.start_old + 1} +{self.start_new},{self.end_new - self.start_new + 1} @@"
@dataclass
class FileDiff:
"""Complete diff for a file."""
filename: str
old_content: list[str]
new_content: list[str]
blocks: list[DiffBlock]
lines: list[DiffLine]
@property
def has_changes(self) -> bool:
return any(line.type in (DiffType.ADDED, DiffType.REMOVED) for line in self.lines)
@property
def additions(self) -> int:
return sum(1 for line in self.lines if line.type == DiffType.ADDED)
@property
def deletions(self) -> int:
return sum(1 for line in self.lines if line.type == DiffType.REMOVED)
class DiffGenerator:
"""Generates diffs between old and new content."""
def __init__(self, context_lines: int = 3):
"""
Initialize diff generator.
Args:
context_lines: Number of context lines around changes
"""
self.context_lines = context_lines
def generate(
self,
old_lines: list[str],
new_lines: list[str],
filename: str = ""
) -> FileDiff:
"""
Generate a diff between old and new content.
Args:
old_lines: Original content lines
new_lines: New content lines
filename: Optional filename for display
Returns:
FileDiff object with all diff information
"""
diff_lines: list[DiffLine] = []
# Use difflib to compute differences
matcher = difflib.SequenceMatcher(None, old_lines, new_lines)
old_line_num = 1
new_line_num = 1
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
if tag == 'equal':
for idx in range(i2 - i1):
diff_lines.append(DiffLine(
type=DiffType.UNCHANGED,
line_number_old=old_line_num,
line_number_new=new_line_num,
content=old_lines[i1 + idx]
))
old_line_num += 1
new_line_num += 1
elif tag == 'replace':
# Show removed lines first, then added
for idx in range(i2 - i1):
diff_lines.append(DiffLine(
type=DiffType.REMOVED,
line_number_old=old_line_num,
line_number_new=None,
content=old_lines[i1 + idx]
))
old_line_num += 1
for idx in range(j2 - j1):
diff_lines.append(DiffLine(
type=DiffType.ADDED,
line_number_old=None,
line_number_new=new_line_num,
content=new_lines[j1 + idx]
))
new_line_num += 1
elif tag == 'delete':
for idx in range(i2 - i1):
diff_lines.append(DiffLine(
type=DiffType.REMOVED,
line_number_old=old_line_num,
line_number_new=None,
content=old_lines[i1 + idx]
))
old_line_num += 1
elif tag == 'insert':
for idx in range(j2 - j1):
diff_lines.append(DiffLine(
type=DiffType.ADDED,
line_number_old=None,
line_number_new=new_line_num,
content=new_lines[j1 + idx]
))
new_line_num += 1
# Generate blocks with context
blocks = self._generate_blocks(diff_lines)
return FileDiff(
filename=filename,
old_content=old_lines,
new_content=new_lines,
blocks=blocks,
lines=diff_lines
)
def _generate_blocks(self, diff_lines: list[DiffLine]) -> list[DiffBlock]:
"""Generate diff blocks with context."""
if not diff_lines:
return []
blocks: list[DiffBlock] = []
current_block_lines: list[DiffLine] = []
in_change = False
unchanged_count = 0
for line in diff_lines:
is_change = line.type in (DiffType.ADDED, DiffType.REMOVED)
if is_change:
if not in_change:
# Starting a new change block, include context
in_change = True
unchanged_count = 0
current_block_lines.append(line)
else: # Unchanged line
if in_change:
unchanged_count += 1
if unchanged_count <= self.context_lines:
current_block_lines.append(line)
else:
# End current block and start fresh
if current_block_lines:
blocks.append(self._create_block(current_block_lines))
current_block_lines = []
in_change = False
unchanged_count = 0
else:
# Keep track of potential context lines
current_block_lines.append(line)
if len(current_block_lines) > self.context_lines:
current_block_lines.pop(0)
# Don't forget the last block
if current_block_lines and any(
l.type in (DiffType.ADDED, DiffType.REMOVED) for l in current_block_lines
):
blocks.append(self._create_block(current_block_lines))
return blocks
def _create_block(self, lines: list[DiffLine]) -> DiffBlock:
"""Create a DiffBlock from a list of lines."""
old_nums = [l.line_number_old for l in lines if l.line_number_old is not None]
new_nums = [l.line_number_new for l in lines if l.line_number_new is not None]
return DiffBlock(
start_old=min(old_nums) if old_nums else 0,
end_old=max(old_nums) if old_nums else 0,
start_new=min(new_nums) if new_nums else 0,
end_new=max(new_nums) if new_nums else 0,
lines=lines
)
def generate_unified_diff(
self,
old_lines: list[str],
new_lines: list[str],
old_filename: str = "a/file",
new_filename: str = "b/file"
) -> str:
"""
Generate a unified diff string.
Args:
old_lines: Original content lines
new_lines: New content lines
old_filename: Label for old file
new_filename: Label for new file
Returns:
Unified diff as string
"""
diff = difflib.unified_diff(
old_lines,
new_lines,
fromfile=old_filename,
tofile=new_filename,
lineterm=""
)
return "\n".join(diff)
def generate_side_by_side(
self,
old_lines: list[str],
new_lines: list[str],
width: int = 80
) -> list[tuple[str, str, str]]:
"""
Generate a side-by-side diff representation.
Args:
old_lines: Original content lines
new_lines: New content lines
width: Width for each column
Returns:
List of tuples (left_line, marker, right_line)
"""
result = []
half_width = (width - 3) // 2
matcher = difflib.SequenceMatcher(None, old_lines, new_lines)
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
if tag == 'equal':
for idx in range(i2 - i1):
line = old_lines[i1 + idx][:half_width]
result.append((line, " ", line))
elif tag == 'replace':
max_len = max(i2 - i1, j2 - j1)
for idx in range(max_len):
old_line = old_lines[i1 + idx][:half_width] if idx < i2 - i1 else ""
new_line = new_lines[j1 + idx][:half_width] if idx < j2 - j1 else ""
result.append((old_line, "|", new_line))
elif tag == 'delete':
for idx in range(i2 - i1):
old_line = old_lines[i1 + idx][:half_width]
result.append((old_line, "<", ""))
elif tag == 'insert':
for idx in range(j2 - j1):
new_line = new_lines[j1 + idx][:half_width]
result.append(("", ">", new_line))
return result
def format_diff_for_display(diff: FileDiff, use_colors: bool = True) -> str:
"""
Format a FileDiff for terminal/GUI display.
Args:
diff: The FileDiff to format
use_colors: Whether to use ANSI colors
Returns:
Formatted string
"""
lines = []
if diff.filename:
lines.append(f"--- {diff.filename}")
lines.append(f"+++ {diff.filename}")
for line in diff.lines:
if use_colors:
if line.type == DiffType.ADDED:
prefix = "\033[32m+" # Green
suffix = "\033[0m"
elif line.type == DiffType.REMOVED:
prefix = "\033[31m-" # Red
suffix = "\033[0m"
else:
prefix = " "
suffix = ""
else:
prefix = line.prefix
suffix = ""
lines.append(f"{prefix} {line.content}{suffix}")
return "\n".join(lines)
def main():
"""Test the diff generator."""
old_content = [
"def calculate_tax(amount):",
" rate = 0.19",
" if amount > 1000:",
" rate = 0.25",
" return amount * rate",
]
new_content = [
"def calculate_tax(amount):",
" rate = 0.19",
" if amount > 10000:",
" rate = 0.22",
" elif amount > 1000:",
" rate = 0.19",
" return amount * rate",
]
generator = DiffGenerator()
diff = generator.generate(old_content, new_content, "calculator.py")
print(f"File: {diff.filename}")
print(f"Additions: {diff.additions}, Deletions: {diff.deletions}")
print()
print("Diff output:")
print(format_diff_for_display(diff))
if __name__ == "__main__":
main()
+584
View File
@@ -0,0 +1,584 @@
"""
DCTP Executor - Executes DCTP operations on files.
Handles CREATE, DELETE, INSERT_AFTER, REPLACE, and RENUMBER operations
with backup support and checksum validation.
"""
import hashlib
import os
import re
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import Optional
from dctp_parser import DCTPParser, Operation, OperationType
from dctp_backup import BackupManager
from dctp_diff import DiffGenerator, FileDiff
class ResultStatus(Enum):
SUCCESS = "success"
WARNING = "warning"
ERROR = "error"
SKIPPED = "skipped"
@dataclass
class ExecutionResult:
"""Result of executing a single operation."""
status: ResultStatus
operation: Operation
message: str
diff: Optional[FileDiff] = None
def __str__(self) -> str:
status_symbols = {
ResultStatus.SUCCESS: "",
ResultStatus.WARNING: "⚠️",
ResultStatus.ERROR: "",
ResultStatus.SKIPPED: "⏭️",
}
return f"{status_symbols[self.status]} {self.message}"
@dataclass
class PreviewResult:
"""Result of previewing operations before execution."""
operation: Operation
description: str
diff: Optional[FileDiff] = None
warnings: list[str] = None
def __post_init__(self):
if self.warnings is None:
self.warnings = []
class DCTPExecutor:
"""Executes DCTP operations on files."""
# Line number patterns (same as parser)
LINE_NUMBER_PATTERNS = [
re.compile(r'\s*#Z(\d+)\s*$'),
re.compile(r'\s*//Z(\d+)\s*$'),
re.compile(r'\s*<!--Z(\d+)-->\s*$'),
re.compile(r'\s*/\*Z(\d+)\*/\s*$'),
re.compile(r'\s*--Z(\d+)\s*$'),
]
def __init__(
self,
project_path: str,
backup_manager: Optional[BackupManager] = None,
auto_renumber: bool = True,
validate_checksums: bool = True
):
"""
Initialize the executor.
Args:
project_path: Base project directory
backup_manager: Optional backup manager for undo support
auto_renumber: Automatically renumber after operations
validate_checksums: Validate checksums before operations
"""
self.project_path = Path(project_path)
self.backup_manager = backup_manager or BackupManager(project_path)
self.auto_renumber = auto_renumber
self.validate_checksums = validate_checksums
self.diff_generator = DiffGenerator()
self.parser = DCTPParser()
def preview(self, operations: list[Operation]) -> list[PreviewResult]:
"""
Preview operations without executing them.
Args:
operations: List of operations to preview
Returns:
List of preview results
"""
previews = []
for op in operations:
preview = self._preview_operation(op)
previews.append(preview)
return previews
def _preview_operation(self, op: Operation) -> PreviewResult:
"""Generate preview for a single operation."""
file_path = self.project_path / op.file
warnings = []
if op.type == OperationType.NEW:
if file_path.exists():
warnings.append(f"File already exists and will be overwritten")
return PreviewResult(
operation=op,
description=f"CREATE {op.file} ({len(op.content)} lines)",
warnings=warnings
)
elif op.type == OperationType.DELETE:
if not file_path.exists():
warnings.append(f"File does not exist")
return PreviewResult(
operation=op,
description=f"DELETE {op.file} Z{op.start_line}-Z{op.end_line} (file not found)",
warnings=warnings
)
old_lines = self._read_file_lines(file_path)
if op.end_line > len(old_lines):
warnings.append(f"Line range exceeds file length ({len(old_lines)} lines)")
new_lines = old_lines.copy()
start_idx = op.start_line - 1
end_idx = min(op.end_line, len(old_lines))
del new_lines[start_idx:end_idx]
diff = self.diff_generator.generate(old_lines, new_lines, op.file)
return PreviewResult(
operation=op,
description=f"DELETE {op.file} Z{op.start_line}-Z{op.end_line}",
diff=diff,
warnings=warnings
)
elif op.type == OperationType.INSERT_AFTER:
if not file_path.exists():
warnings.append(f"File does not exist")
return PreviewResult(
operation=op,
description=f"INSERT_AFTER {op.file} Z{op.start_line} (file not found)",
warnings=warnings
)
old_lines = self._read_file_lines(file_path)
if op.start_line > len(old_lines):
warnings.append(f"Line {op.start_line} exceeds file length ({len(old_lines)} lines)")
new_lines = old_lines.copy()
insert_idx = min(op.start_line, len(old_lines))
for i, line in enumerate(op.content):
new_lines.insert(insert_idx + i, line)
diff = self.diff_generator.generate(old_lines, new_lines, op.file)
return PreviewResult(
operation=op,
description=f"INSERT_AFTER {op.file} Z{op.start_line} ({len(op.content)} lines)",
diff=diff,
warnings=warnings
)
elif op.type == OperationType.REPLACE:
if not file_path.exists():
warnings.append(f"File does not exist")
return PreviewResult(
operation=op,
description=f"REPLACE {op.file} Z{op.start_line}-Z{op.end_line} (file not found)",
warnings=warnings
)
old_lines = self._read_file_lines(file_path)
if op.end_line > len(old_lines):
warnings.append(f"Line range exceeds file length ({len(old_lines)} lines)")
new_lines = old_lines.copy()
start_idx = op.start_line - 1
end_idx = min(op.end_line, len(old_lines))
new_lines[start_idx:end_idx] = op.content
diff = self.diff_generator.generate(old_lines, new_lines, op.file)
return PreviewResult(
operation=op,
description=f"REPLACE {op.file} Z{op.start_line}-Z{op.end_line} ({len(op.content)} lines)",
diff=diff,
warnings=warnings
)
elif op.type == OperationType.RENUMBER:
return PreviewResult(
operation=op,
description=f"RENUMBER {op.file}",
warnings=warnings
)
return PreviewResult(
operation=op,
description=f"UNKNOWN {op.type}",
warnings=["Unknown operation type"]
)
def execute(
self,
operations: list[Operation],
skip_checksum_mismatch: bool = False
) -> list[ExecutionResult]:
"""
Execute a list of operations.
Args:
operations: List of operations to execute
skip_checksum_mismatch: Continue even if checksums don't match
Returns:
List of execution results
"""
results = []
# Start backup session
self.backup_manager.start_session()
try:
for op in operations:
result = self._execute_operation(op, skip_checksum_mismatch)
results.append(result)
# Stop on error
if result.status == ResultStatus.ERROR:
break
finally:
# End backup session
self.backup_manager.end_session()
return results
def _execute_operation(
self,
op: Operation,
skip_checksum_mismatch: bool = False
) -> ExecutionResult:
"""Execute a single operation."""
file_path = self.project_path / op.file
try:
if op.type == OperationType.NEW:
return self._execute_new(op, file_path)
elif op.type == OperationType.DELETE:
return self._execute_delete(op, file_path, skip_checksum_mismatch)
elif op.type == OperationType.INSERT_AFTER:
return self._execute_insert_after(op, file_path, skip_checksum_mismatch)
elif op.type == OperationType.REPLACE:
return self._execute_replace(op, file_path, skip_checksum_mismatch)
elif op.type == OperationType.RENUMBER:
return self._execute_renumber(op, file_path)
else:
return ExecutionResult(
status=ResultStatus.ERROR,
operation=op,
message=f"Unknown operation type: {op.type}"
)
except PermissionError:
return ExecutionResult(
status=ResultStatus.ERROR,
operation=op,
message=f"Permission denied: {file_path}"
)
except Exception as e:
return ExecutionResult(
status=ResultStatus.ERROR,
operation=op,
message=f"Error: {str(e)}"
)
def _execute_new(self, op: Operation, file_path: Path) -> ExecutionResult:
"""Execute a NEW operation (create file)."""
# Backup if file exists
if file_path.exists():
self.backup_manager.backup(str(op.file))
# Create parent directories
file_path.parent.mkdir(parents=True, exist_ok=True)
# Write content
content = "\n".join(op.content)
if op.content and not content.endswith("\n"):
content += "\n"
file_path.write_text(content)
return ExecutionResult(
status=ResultStatus.SUCCESS,
operation=op,
message=f"CREATE {op.file} ({len(op.content)} lines)"
)
def _execute_delete(
self,
op: Operation,
file_path: Path,
skip_checksum_mismatch: bool
) -> ExecutionResult:
"""Execute a DELETE operation."""
if not file_path.exists():
return ExecutionResult(
status=ResultStatus.WARNING,
operation=op,
message=f"File not found: {op.file}"
)
# Validate checksum if provided
if op.checksum and self.validate_checksums:
if not self._validate_checksum(file_path, op.checksum):
if not skip_checksum_mismatch:
return ExecutionResult(
status=ResultStatus.WARNING,
operation=op,
message=f"Checksum mismatch for {op.file} - file was modified externally"
)
# Backup file
self.backup_manager.backup(str(op.file))
# Read file and delete lines
lines = self._read_file_lines(file_path)
old_lines = lines.copy()
if op.end_line > len(lines):
return ExecutionResult(
status=ResultStatus.WARNING,
operation=op,
message=f"Line range Z{op.start_line}-Z{op.end_line} exceeds file length ({len(lines)} lines)"
)
start_idx = op.start_line - 1
end_idx = op.end_line
del lines[start_idx:end_idx]
# Write back
self._write_file_lines(file_path, lines)
diff = self.diff_generator.generate(old_lines, lines, op.file)
return ExecutionResult(
status=ResultStatus.SUCCESS,
operation=op,
message=f"DELETE {op.file} Z{op.start_line}-Z{op.end_line}",
diff=diff
)
def _execute_insert_after(
self,
op: Operation,
file_path: Path,
skip_checksum_mismatch: bool
) -> ExecutionResult:
"""Execute an INSERT_AFTER operation."""
if not file_path.exists():
return ExecutionResult(
status=ResultStatus.WARNING,
operation=op,
message=f"File not found: {op.file}"
)
# Validate checksum if provided
if op.checksum and self.validate_checksums:
if not self._validate_checksum(file_path, op.checksum):
if not skip_checksum_mismatch:
return ExecutionResult(
status=ResultStatus.WARNING,
operation=op,
message=f"Checksum mismatch for {op.file} - file was modified externally"
)
# Backup file
self.backup_manager.backup(str(op.file))
# Read file and insert lines
lines = self._read_file_lines(file_path)
old_lines = lines.copy()
if op.start_line > len(lines):
return ExecutionResult(
status=ResultStatus.WARNING,
operation=op,
message=f"Line Z{op.start_line} exceeds file length ({len(lines)} lines)"
)
insert_idx = op.start_line
for i, line in enumerate(op.content):
lines.insert(insert_idx + i, line)
# Write back
self._write_file_lines(file_path, lines)
diff = self.diff_generator.generate(old_lines, lines, op.file)
return ExecutionResult(
status=ResultStatus.SUCCESS,
operation=op,
message=f"INSERT_AFTER {op.file} Z{op.start_line} ({len(op.content)} lines)",
diff=diff
)
def _execute_replace(
self,
op: Operation,
file_path: Path,
skip_checksum_mismatch: bool
) -> ExecutionResult:
"""Execute a REPLACE operation."""
if not file_path.exists():
return ExecutionResult(
status=ResultStatus.WARNING,
operation=op,
message=f"File not found: {op.file}"
)
# Validate checksum if provided
if op.checksum and self.validate_checksums:
if not self._validate_checksum(file_path, op.checksum):
if not skip_checksum_mismatch:
return ExecutionResult(
status=ResultStatus.WARNING,
operation=op,
message=f"Checksum mismatch for {op.file} - file was modified externally"
)
# Backup file
self.backup_manager.backup(str(op.file))
# Read file and replace lines
lines = self._read_file_lines(file_path)
old_lines = lines.copy()
if op.end_line > len(lines):
return ExecutionResult(
status=ResultStatus.WARNING,
operation=op,
message=f"Line range Z{op.start_line}-Z{op.end_line} exceeds file length ({len(lines)} lines)"
)
start_idx = op.start_line - 1
end_idx = op.end_line
lines[start_idx:end_idx] = op.content
# Write back
self._write_file_lines(file_path, lines)
diff = self.diff_generator.generate(old_lines, lines, op.file)
return ExecutionResult(
status=ResultStatus.SUCCESS,
operation=op,
message=f"REPLACE {op.file} Z{op.start_line}-Z{op.end_line} ({len(op.content)} lines)",
diff=diff
)
def _execute_renumber(self, op: Operation, file_path: Path) -> ExecutionResult:
"""Execute a RENUMBER operation."""
if not file_path.exists():
return ExecutionResult(
status=ResultStatus.WARNING,
operation=op,
message=f"File not found: {op.file}"
)
# No backup needed for renumber (just updates line numbers)
lines = self._read_file_lines(file_path)
renumbered_lines = []
for i, line in enumerate(lines, 1):
# Remove existing line number
clean_line = self._remove_line_number(line)
# Add new line number
suffix = self.parser.get_line_number_suffix(op.file, i)
renumbered_lines.append(clean_line + suffix)
self._write_file_lines(file_path, renumbered_lines)
return ExecutionResult(
status=ResultStatus.SUCCESS,
operation=op,
message=f"RENUMBER {op.file} ({len(lines)} lines)"
)
def _read_file_lines(self, file_path: Path) -> list[str]:
"""Read file and return lines without trailing newlines."""
content = file_path.read_text()
lines = content.split('\n')
# Remove trailing empty line if file ends with newline
if lines and lines[-1] == '':
lines = lines[:-1]
return lines
def _write_file_lines(self, file_path: Path, lines: list[str]) -> None:
"""Write lines to file with trailing newline."""
content = '\n'.join(lines)
if lines and not content.endswith('\n'):
content += '\n'
file_path.write_text(content)
def _remove_line_number(self, line: str) -> str:
"""Remove line number marker from end of line."""
for pattern in self.LINE_NUMBER_PATTERNS:
match = pattern.search(line)
if match:
return line[:match.start()]
return line
def _validate_checksum(self, file_path: Path, expected: str) -> bool:
"""Validate file checksum."""
content = file_path.read_bytes()
actual = hashlib.md5(content).hexdigest()[:8]
return actual.lower() == expected.lower()
@staticmethod
def calculate_checksum(file_path: Path) -> str:
"""Calculate checksum for a file."""
content = file_path.read_bytes()
return hashlib.md5(content).hexdigest()[:8]
def main():
"""Test the executor."""
import tempfile
test_input = """###FILE:calculator.py
###NEW
def add(a, b): #Z1
return a + b #Z2
#Z3
def multiply(a, b): #Z4
return a * b #Z5
###END
"""
with tempfile.TemporaryDirectory() as tmpdir:
parser = DCTPParser()
result = parser.parse(test_input)
print("Parsed operations:")
for op in result.operations:
print(f" {op}")
executor = DCTPExecutor(tmpdir)
# Preview
print("\nPreviews:")
previews = executor.preview(result.operations)
for preview in previews:
print(f" {preview.description}")
if preview.warnings:
for w in preview.warnings:
print(f" ⚠️ {w}")
# Execute
print("\nExecution:")
exec_results = executor.execute(result.operations)
for r in exec_results:
print(f" {r}")
# Verify file was created
file_path = Path(tmpdir) / "calculator.py"
if file_path.exists():
print(f"\nFile content:\n{file_path.read_text()}")
if __name__ == "__main__":
main()
+666
View File
@@ -0,0 +1,666 @@
#!/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()
+307
View File
@@ -0,0 +1,307 @@
"""
DCTP Parser - Parses DCTP control commands and code blocks.
Handles line-numbered code with language-specific comment formats:
- Python/Shell: #Z1
- JavaScript/Java/C/C++: //Z1
- HTML: <!--Z1-->
- CSS: /*Z1*/
- SQL: --Z1
"""
import re
from dataclasses import dataclass, field
from typing import Optional
from enum import Enum
class OperationType(Enum):
NEW = "NEW"
DELETE = "DELETE"
INSERT_AFTER = "INSERT_AFTER"
REPLACE = "REPLACE"
RENUMBER = "RENUMBER"
@dataclass
class Operation:
"""Represents a single DCTP operation."""
type: OperationType
file: str
start_line: Optional[int] = None
end_line: Optional[int] = None
content: list[str] = field(default_factory=list)
checksum: Optional[str] = None
raw_content: list[str] = field(default_factory=list) # Content with line numbers
def __str__(self) -> str:
if self.type == OperationType.NEW:
return f"CREATE {self.file} ({len(self.content)} lines)"
elif self.type == OperationType.DELETE:
return f"DELETE {self.file} Z{self.start_line}-Z{self.end_line}"
elif self.type == OperationType.INSERT_AFTER:
return f"INSERT_AFTER {self.file} Z{self.start_line} ({len(self.content)} lines)"
elif self.type == OperationType.REPLACE:
return f"REPLACE {self.file} Z{self.start_line}-Z{self.end_line} ({len(self.content)} lines)"
elif self.type == OperationType.RENUMBER:
return f"RENUMBER {self.file}"
return f"{self.type.value} {self.file}"
@dataclass
class ParseError:
"""Represents a parsing error."""
line_number: int
line_content: str
message: str
@dataclass
class ParseResult:
"""Result of parsing DCTP input."""
operations: list[Operation]
errors: list[ParseError]
@property
def has_errors(self) -> bool:
return len(self.errors) > 0
class DCTPParser:
"""Parser for DCTP (Delta Code Transfer Protocol) format."""
# Regex patterns for line number markers in different languages
LINE_NUMBER_PATTERNS = [
re.compile(r'\s*#Z(\d+)\s*$'), # Python, Shell
re.compile(r'\s*//Z(\d+)\s*$'), # JavaScript, Java, C, C++
re.compile(r'\s*<!--Z(\d+)-->\s*$'), # HTML
re.compile(r'\s*/\*Z(\d+)\*/\s*$'), # CSS
re.compile(r'\s*--Z(\d+)\s*$'), # SQL
]
# Control command patterns
FILE_PATTERN = re.compile(r'^###FILE:(.+)$')
NEW_PATTERN = re.compile(r'^###NEW\s*$')
DELETE_PATTERN = re.compile(r'^###DELETE:Z(\d+)(?:-Z(\d+))?\s*$')
INSERT_AFTER_PATTERN = re.compile(r'^###INSERT_AFTER:Z(\d+)\s*$')
REPLACE_PATTERN = re.compile(r'^###REPLACE:Z(\d+)(?:-Z(\d+))?\s*$')
END_PATTERN = re.compile(r'^###END\s*$')
RENUMBER_PATTERN = re.compile(r'^###RENUMBER\s*$')
CHECKSUM_PATTERN = re.compile(r'^###CHECKSUM:([a-fA-F0-9]+)\s*$')
def parse(self, text: str) -> ParseResult:
"""
Parse DCTP formatted text into a list of operations.
Args:
text: The DCTP formatted input text
Returns:
ParseResult containing operations and any errors
"""
operations: list[Operation] = []
errors: list[ParseError] = []
current_file: Optional[str] = None
current_op: Optional[Operation] = None
buffer: list[str] = []
raw_buffer: list[str] = []
lines = text.split('\n')
for line_num, line in enumerate(lines, 1):
# Skip empty lines outside of content blocks
if not line.strip() and current_op is None:
continue
# Check for FILE command
file_match = self.FILE_PATTERN.match(line)
if file_match:
current_file = file_match.group(1).strip()
continue
# Check for NEW command
if self.NEW_PATTERN.match(line):
if current_file is None:
errors.append(ParseError(line_num, line, "###NEW without ###FILE"))
continue
current_op = Operation(type=OperationType.NEW, file=current_file)
buffer = []
raw_buffer = []
continue
# Check for DELETE command
delete_match = self.DELETE_PATTERN.match(line)
if delete_match:
if current_file is None:
errors.append(ParseError(line_num, line, "###DELETE without ###FILE"))
continue
start = int(delete_match.group(1))
end = int(delete_match.group(2)) if delete_match.group(2) else start
operations.append(Operation(
type=OperationType.DELETE,
file=current_file,
start_line=start,
end_line=end
))
continue
# Check for INSERT_AFTER command
insert_match = self.INSERT_AFTER_PATTERN.match(line)
if insert_match:
if current_file is None:
errors.append(ParseError(line_num, line, "###INSERT_AFTER without ###FILE"))
continue
current_op = Operation(
type=OperationType.INSERT_AFTER,
file=current_file,
start_line=int(insert_match.group(1))
)
buffer = []
raw_buffer = []
continue
# Check for REPLACE command
replace_match = self.REPLACE_PATTERN.match(line)
if replace_match:
if current_file is None:
errors.append(ParseError(line_num, line, "###REPLACE without ###FILE"))
continue
start = int(replace_match.group(1))
end = int(replace_match.group(2)) if replace_match.group(2) else start
current_op = Operation(
type=OperationType.REPLACE,
file=current_file,
start_line=start,
end_line=end
)
buffer = []
raw_buffer = []
continue
# Check for END command
if self.END_PATTERN.match(line):
if current_op:
current_op.content = buffer.copy()
current_op.raw_content = raw_buffer.copy()
operations.append(current_op)
current_op = None
buffer = []
raw_buffer = []
continue
# Check for RENUMBER command
if self.RENUMBER_PATTERN.match(line):
if current_file is None:
errors.append(ParseError(line_num, line, "###RENUMBER without ###FILE"))
continue
operations.append(Operation(type=OperationType.RENUMBER, file=current_file))
continue
# Check for CHECKSUM command
checksum_match = self.CHECKSUM_PATTERN.match(line)
if checksum_match:
if current_op:
current_op.checksum = checksum_match.group(1)
continue
# Regular code line - add to buffer if we're in an operation
if current_op is not None:
raw_buffer.append(line)
clean_line = self._remove_line_number(line)
buffer.append(clean_line)
# Handle unclosed operation
if current_op is not None:
errors.append(ParseError(
len(lines),
"",
f"Unclosed operation: {current_op.type.value} for {current_op.file}"
))
return ParseResult(operations=operations, errors=errors)
def _remove_line_number(self, line: str) -> str:
"""Remove line number marker from end of line."""
for pattern in self.LINE_NUMBER_PATTERNS:
match = pattern.search(line)
if match:
return line[:match.start()]
return line
def extract_line_number(self, line: str) -> Optional[int]:
"""Extract line number from a code line."""
for pattern in self.LINE_NUMBER_PATTERNS:
match = pattern.search(line)
if match:
return int(match.group(1))
return None
@staticmethod
def get_line_number_suffix(filename: str, line_num: int) -> str:
"""Get the appropriate line number suffix for a file type."""
ext = filename.rsplit('.', 1)[-1].lower() if '.' in filename else ''
if ext in ('py', 'sh', 'bash', 'zsh', 'yaml', 'yml', 'toml', 'ini', 'conf', 'rb', 'pl'):
return f" #Z{line_num}"
elif ext in ('js', 'ts', 'jsx', 'tsx', 'java', 'c', 'cpp', 'h', 'hpp', 'cs', 'go', 'rs', 'swift', 'kt', 'scala'):
return f" //Z{line_num}"
elif ext in ('html', 'htm', 'xml', 'svg'):
return f" <!--Z{line_num}-->"
elif ext in ('css', 'scss', 'sass', 'less'):
return f" /*Z{line_num}*/"
elif ext in ('sql',):
return f" --Z{line_num}"
else:
# Default to Python style
return f" #Z{line_num}"
def main():
"""Test the parser with example input."""
test_input = """###FILE:src/calculator.py
###NEW
def add(a, b): #Z1
return a + b #Z2
#Z3
def multiply(a, b): #Z4
return a * b #Z5
###END
###FILE:src/calculator.py
###REPLACE:Z4-Z5
def multiply(a, b): #Z4
\"\"\"Multipliziert zwei Zahlen.\"\"\" #Z5
return a * b #Z6
###END
###RENUMBER
###FILE:src/calculator.py
###INSERT_AFTER:Z2
#Z3
def subtract(a, b): #Z4
return a - b #Z5
###END
###RENUMBER
###FILE:src/calculator.py
###DELETE:Z10-Z15
###RENUMBER
"""
parser = DCTPParser()
result = parser.parse(test_input)
print("Operations found:")
for op in result.operations:
print(f" {op}")
if result.has_errors:
print("\nErrors:")
for error in result.errors:
print(f" Line {error.line_number}: {error.message}")
print(f" {error.line_content}")
if __name__ == "__main__":
main()
+1
View File
@@ -0,0 +1 @@
customtkinter>=5.2.0