From 6b4f19c2dc57c645ed3e43635bd3a0fb12c56908 Mon Sep 17 00:00:00 2001 From: cseyfferth Date: Thu, 3 Jul 2025 17:14:52 +0200 Subject: [PATCH] Initial commit: Python dotfiles system --- .gitignore | 19 ++ README.md | 271 +++++++++++++++ config.yaml | 134 ++++++++ dotfiles.py | 309 +++++++++++++++++ requirements.txt | 6 + src/__init__.py | 9 + src/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 404 bytes src/__pycache__/config.cpython-313.pyc | Bin 0 -> 12016 bytes src/__pycache__/sync.cpython-313.pyc | Bin 0 -> 20330 bytes src/__pycache__/templates.cpython-313.pyc | Bin 0 -> 11134 bytes src/config.py | 231 +++++++++++++ src/sync.py | 397 ++++++++++++++++++++++ src/templates.py | 318 +++++++++++++++++ templates/git/gitconfig | 4 + templates/git/gitconfig.j2 | 49 +++ templates/shell/bashrc | 23 ++ templates/shell/bashrc.j2 | 39 +++ templates/shell/zshrc | 85 +++++ templates/ssh/config | 230 +++++++++++++ templates/vim/vimrc.j2 | 61 ++++ 20 files changed, 2185 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 config.yaml create mode 100755 dotfiles.py create mode 100644 requirements.txt create mode 100644 src/__init__.py create mode 100644 src/__pycache__/__init__.cpython-313.pyc create mode 100644 src/__pycache__/config.cpython-313.pyc create mode 100644 src/__pycache__/sync.cpython-313.pyc create mode 100644 src/__pycache__/templates.cpython-313.pyc create mode 100644 src/config.py create mode 100644 src/sync.py create mode 100644 src/templates.py create mode 100644 templates/git/gitconfig create mode 100644 templates/git/gitconfig.j2 create mode 100644 templates/shell/bashrc create mode 100644 templates/shell/bashrc.j2 create mode 100644 templates/shell/zshrc create mode 100644 templates/ssh/config create mode 100644 templates/vim/vimrc.j2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a1fde1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# Temporäre Dateien +*.tmp +*.log +*.swp +*.swo +*~ + +# System-Dateien +.DS_Store +Thumbs.db + +# Backup-Dateien +*.backup.* + +# Lokale Konfiguration (optional) +local.yaml + +# Persönliche Transcrypt-Schlüssel (sollten nicht ins Repo) +.transcrypt_key \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7c1bc9c --- /dev/null +++ b/README.md @@ -0,0 +1,271 @@ +# Dotfiles - Multi-Machine Configuration Management + +Ein **einfaches** und sicheres System zur Verwaltung von Konfigurationsdateien (Dotfiles) über mehrere Rechner hinweg. Nach dem **KISS-Prinzip**: Keep It Simple, Stupid. + +## ✨ Features + +- 🖥️ **Multi-Machine Support**: Separate Konfigurationen für Desktop, Laptop, Server +- 🔗 **Symlink-basiert**: Keine Kopien, direkte Verknüpfungen zu deinen Configs +- 🔐 **Sichere Verschlüsselung**: Sensible Daten mit transcrypt verschlüsselt +- ⚙️ **Konfigurationsgesteuert**: Alles wird über `config.yaml` definiert +- 🚀 **Ein Skript für alles**: Nur noch ein einziges `dotfiles` Kommando +- 🎯 **Automatische Erkennung**: Maschinenerkennung ohne manuelle Konfiguration + +## 📂 Struktur + +``` +configs/ +├── machines/ # Maschinenspezifische Konfigurationen +│ ├── desktop/ # Desktop-Computer Configs +│ ├── laptop/ # Laptop/Notebook Configs +│ └── server/ # Server-System Configs +├── shared/ # Geteilte Konfigurationen +│ ├── shell/ # Shell-Configs (bash, zsh, etc.) +│ ├── vim/ # Vim-Konfiguration +│ ├── git/ # Git-Konfiguration +│ └── ssh/ # SSH-Konfiguration (öffentlich) +├── sensitive/ # Verschlüsselte sensible Daten +├── dotfiles # ⭐ DAS zentrale Management-Skript +├── config.yaml # ⚙️ Zentrale Konfiguration (definiert alles!) +└── README.md # Diese Dokumentation +``` + +## 🚀 Schnellstart + +### 1. Repository klonen + +```bash +git clone ~/.dotfiles +cd ~/.dotfiles +``` + +### 2. Dotfiles installieren + +```bash +./dotfiles install +``` + +### 3. Aktuelle Configs sichern + +```bash +./dotfiles push +``` + +### 4. Verschlüsselung einrichten (optional) + +```bash +./dotfiles setup-transcrypt +``` + +## 📋 Alle Befehle + +### Installation und Management +```bash +./dotfiles install # Installiere alle Dotfiles (Symlinks) +./dotfiles push # Sammle und committe aktuelle Configs +./dotfiles push --push # Sammle, committe und push zu Remote +./dotfiles list # Zeige installierte Konfigurationen +``` + +### Verschlüsselung +```bash +./dotfiles setup-transcrypt # Interaktive Transcrypt-Einrichtung +./dotfiles setup-transcrypt --auto-password # Mit automatischem Passwort +./dotfiles setup-transcrypt --force # Erneut konfigurieren +``` + +### Hilfe +```bash +./dotfiles help # Zeige alle verfügbaren Befehle +``` + +## ⚙️ Konfiguration über config.yaml + +**Das Herzstück**: Alle Symlinks, Push-Quellen und Einstellungen werden in `config.yaml` definiert! + +### Neue Konfiguration hinzufügen + +Bearbeite `config.yaml` und füge hinzu: + +```yaml +# In der symlinks -> shared Sektion: +- src: "shared/tmux/tmux.conf" + dest: "~/.tmux.conf" + description: "Tmux-Konfiguration" + +# In der push_sources -> shared Sektion: +- src: "~/.tmux.conf" + dest: "shared/tmux/tmux.conf" + description: "Tmux-Konfiguration" +``` + +### Maschinenspezifische Configs + +```yaml +# Für Desktop-spezifische Konfigurationen: +- src: "machines/{machine}/i3_config" + dest: "~/.config/i3/config" + description: "i3-Konfiguration" +``` + +### Vordefinierte Konfigurationen + +`config.yaml` enthält bereits Definitionen für: +- tmux +- neovim +- vscode +- i3 + +## 🔐 Sichere Daten + +### Automatische Verschlüsselung + +Alle Dateien in folgenden Orten werden automatisch verschlüsselt: +- `sensitive/` - Allgemeine sensible Daten +- `machines/*/secrets/` - Maschinenspezifische Geheimnisse +- Dateien mit Namen wie `*secret*`, `*.key`, `*.pem` +- SSH private keys (`id_rsa`, `id_ed25519`, etc.) + +### Sensible Dateien hinzufügen + +```bash +# Einfach in die entsprechenden Verzeichnisse kopieren +cp ~/.ssh/id_rsa sensitive/ssh/id_rsa # Wird automatisch verschlüsselt +cp ~/.env sensitive/env/myproject.env # Wird automatisch verschlüsselt +echo "API_KEY=secret123" > sensitive/api_keys.txt # Wird automatisch verschlüsselt +``` + +## 🔧 Konfigurationen hinzufügen/entfernen + +### Neue Konfiguration hinzufügen + +1. **Datei manuell kopieren**: + ```bash + mkdir -p shared/tmux + cp ~/.tmux.conf shared/tmux/tmux.conf + ``` + +2. **config.yaml erweitern**: + - Symlink-Definition zu `symlinks -> shared` hinzufügen + - Push-Definition zu `push_sources -> shared` hinzufügen + +3. **Testen und sichern**: + ```bash + ./dotfiles install # Test der neuen Symlinks + ./dotfiles push # Änderungen sichern + ``` + +### Konfiguration entfernen + +1. **Aus config.yaml entfernen** (entsprechende Zeilen löschen) +2. **Symlink entfernen**: `rm ~/.tmux.conf` +3. **Aus Repository entfernen**: `rm -rf shared/tmux/` +4. **Backup wiederherstellen** (falls vorhanden): `mv ~/.tmux.conf.backup.* ~/.tmux.conf` + +## 📚 Beispiel-Workflows + +### Neuer Computer einrichten + +```bash +# 1. Repository klonen +git clone git@github.com:username/dotfiles.git ~/.dotfiles +cd ~/.dotfiles + +# 2. Transcrypt entschlüsseln (falls vorhanden) +transcrypt -y + +# 3. Dotfiles installieren +./dotfiles install + +# 4. Shell neu starten +exec $SHELL +``` + +### Konfiguration ändern und teilen + +```bash +# 1. Lokale Änderungen machen +vim ~/.vimrc + +# 2. Ins Repository sichern und pushen +./dotfiles push --push + +# 3. Auf anderen Maschinen updaten +git pull +./dotfiles install # Falls neue Dateien hinzugekommen sind +``` + +### Neue Anwendung hinzufügen (Beispiel: Tmux) + +```bash +# 1. Konfiguration ins Repository kopieren +mkdir -p shared/tmux +cp ~/.tmux.conf shared/tmux/tmux.conf + +# 2. config.yaml bearbeiten (Definitionen hinzufügen) +vim config.yaml + +# 3. Testen +./dotfiles install + +# 4. Sichern +./dotfiles push +``` + +## 🔍 Troubleshooting + +### Problem: "YAML-Parser-Fehler" +```bash +# Prüfe YAML-Syntax +python3 -c "import yaml; yaml.safe_load(open('config.yaml'))" +``` + +### Problem: "Symlink funktioniert nicht" +```bash +# Prüfe Symlink-Ziel +readlink ~/.vimrc + +# Liste installierte Symlinks +./dotfiles list +``` + +### Problem: "Transcrypt-Fehler" +```bash +# Status prüfen +transcrypt --display + +# Neu konfigurieren +./dotfiles setup-transcrypt --force +``` + +## ⚠️ Wichtige Hinweise + +1. **config.yaml ist der Schlüssel**: Alle Änderungen werden dort definiert +2. **Backup**: Existierende Dateien werden automatisch gesichert +3. **KISS-Prinzip**: Ein Skript, eine Konfigurationsdatei - einfach! +4. **Transcrypt-Passwort**: Sicher aufbewahren! + +## 🎯 Migration von alten Dotfiles-Systemen + +Falls du von einem anderen Dotfiles-System migrierst: + +```bash +# 1. Aktuelle Symlinks entfernen +find ~ -maxdepth 2 -type l -delete + +# 2. Konfigurationen ins neue System kopieren +cp ~/.vimrc shared/vim/vimrc +cp ~/.gitconfig shared/git/gitconfig +# ... weitere Dateien + +# 3. config.yaml entsprechend anpassen + +# 4. Neu installieren +./dotfiles install +``` + +--- + +**Das war's!** Ein Skript, eine Konfigurationsdatei - dotfiles management kann so einfach sein! 🚀 + +Nach dem **KISS-Prinzip**: Keep It Simple, Stupid. ✨ \ No newline at end of file diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..b2718dd --- /dev/null +++ b/config.yaml @@ -0,0 +1,134 @@ +backup: + enabled: true + format: .backup.{timestamp} + keep_backups: 5 +current_machine: laptop +encryption: + enabled: false + patterns: + - sensitive/** + - machines/*/secrets/** + - '**/*_secret*' + - '**/*secret*' + - '**/*.key' + - '**/*.pem' + - '**/*.p12' + - '**/*.pfx' + - '**/passwords.*' + - '**/credentials.*' + - '**/.env' + - '**/.env.*' + - '**/authinfo' + - '**/netrc' + - '**/*_rsa' + - '**/*_ed25519' + - '**/*_ecdsa' + - '**/id_rsa' + - '**/id_ed25519' + - '**/id_ecdsa' +git: + auto_commit: true + commit_message_template: Update dotfiles from {machine} - {timestamp} + excluded_files: + - '*.log' + - '*.tmp' + - .DS_Store + - Thumbs.db + - '*.backup.*' + - __pycache__/ + - '*.pyc' +machine_variables: + desktop: + font_size: 12 + terminal: gnome-terminal + window_manager: i3 + laptop: + font_size: 10 + power_management: true + terminal: alacritty + window_manager: i3 + server: + font_size: 8 + headless: true + terminal: tmux +machines: + desktop: + description: Desktop-Computer + hostname_patterns: + - desktop + - pc + - workstation + os_patterns: + - Linux + - Windows + laptop: + description: Laptop/Notebook + hostname_patterns: + - laptop + - notebook + - mobile + - p14s + os_patterns: + - Linux + - Darwin + - Windows + server: + description: Server-System + hostname_patterns: + - server + - srv + - vps + os_patterns: + - Linux +sync: + create_missing_dirs: true + dry_run: false + force_overwrite: false + interactive: true +templates: + git: + - description: Git-Konfiguration + destination: ~/.gitconfig + template: git/gitconfig.j2 + - description: Git-Global-Ignore + destination: ~/.gitignore_global + template: git/gitignore_global + i3: + - description: i3-Konfiguration + destination: ~/.config/i3/config + machine_specific: true + template: i3/config.{machine}.j2 + nvim: + - description: Neovim-Konfiguration + destination: ~/.config/nvim/init.lua + template: nvim/init.lua.j2 + shell: + - description: Bash-Konfiguration + destination: ~/.bashrc + template: shell/bashrc.j2 + - description: Zsh-Konfiguration + destination: ~/.zshrc + template: shell/zshrc.j2 + - description: Shell-Profile + destination: ~/.profile + template: shell/profile.j2 + ssh: + - description: SSH-Konfiguration + destination: ~/.ssh/config + encrypted: true + mode: '0600' + template: ssh/config.j2 + tmux: + - description: Tmux-Konfiguration + destination: ~/.tmux.conf + template: tmux/tmux.conf.j2 + vim: + - description: Vim-Konfiguration + destination: ~/.vimrc + template: vim/vimrc.j2 +variables: + browser: firefox + editor: vim + git_email: your.email@example.com + git_name: Your Name + terminal: alacritty diff --git a/dotfiles.py b/dotfiles.py new file mode 100755 index 0000000..28da6a5 --- /dev/null +++ b/dotfiles.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python3 +""" +Dotfiles Management System + +Ein modernes Python-System zur Verwaltung von Konfigurationsdateien +ohne Symlinks - mit echter Datei-Synchronisation und Template-System. +""" + +import sys +import click +from pathlib import Path +from rich.console import Console +from rich.table import Table +from rich import print as rprint + +# Füge src/ zum Python-Pfad hinzu +sys.path.insert(0, str(Path(__file__).parent / "src")) + +from src.config import ConfigManager +from src.sync import FileSynchronizer +from src.templates import TemplateManager, create_example_templates + +console = Console() + + +@click.group() +@click.version_option(version="2.0.0") +@click.pass_context +def cli(ctx): + """ + 🐍 Dotfiles Management System v2.0 + + Modernes Python-System zur Verwaltung von Konfigurationsdateien + ohne Symlinks - mit echter Datei-Synchronisation und Template-System. + """ + try: + ctx.ensure_object(dict) + ctx.obj['config'] = ConfigManager() + ctx.obj['sync'] = FileSynchronizer(ctx.obj['config']) + ctx.obj['templates'] = TemplateManager(ctx.obj['config']) + + except Exception as e: + rprint(f"[red]❌ Fehler beim Initialisieren: {e}[/red]") + sys.exit(1) + + +@cli.command() +@click.option('--dry-run', '-n', is_flag=True, help='Zeige nur was gemacht würde') +@click.option('--force', '-f', is_flag=True, help='Überschreibe ohne Bestätigung') +@click.option('--no-interactive', is_flag=True, help='Keine interaktiven Abfragen') +@click.pass_context +def install(ctx, dry_run, force, no_interactive): + """ + 📦 Installiert Templates ins Home-Verzeichnis + + Kopiert alle konfigurierten Template-Dateien ins Home-Verzeichnis + und rendert dabei Jinja2-Templates mit maschinenspezifischen Variablen. + """ + config = ctx.obj['config'] + sync = ctx.obj['sync'] + + if not config.current_machine: + rprint("[red]❌ Keine Maschine erkannt![/red]") + sys.exit(1) + + rprint(f"[green]🖥️ Aktuelle Maschine: {config.current_machine}[/green]") + + # Installation ausführen + result = sync.install( + dry_run=dry_run, + force=force, + interactive=not no_interactive + ) + + if result.errors: + sys.exit(1) + + +@cli.command() +@click.option('--dry-run', '-n', is_flag=True, help='Zeige nur was gemacht würde') +@click.option('--push', is_flag=True, help='Pushe Änderungen automatisch ins Git-Repository') +@click.pass_context +def backup(ctx, dry_run, push): + """ + 💾 Sichert Dateien vom Home-Verzeichnis ins Repository + + Kopiert aktuelle Konfigurationsdateien zurück ins Repository + um Änderungen zu sichern. Mit --push werden die Änderungen + automatisch committet und gepusht. + """ + sync = ctx.obj['sync'] + + result = sync.backup(dry_run=dry_run, git_push=push) + + if result.errors: + sys.exit(1) + + +@cli.command() +@click.pass_context +def status(ctx): + """ + 📊 Zeigt Status aller Konfigurationsdateien + + Vergleicht Template-Dateien mit installierten Dateien + und zeigt Unterschiede an. + """ + sync = ctx.obj['sync'] + sync.status() + + +@cli.command() +@click.pass_context +def templates(ctx): + """ + 📝 Listet alle verfügbaren Templates auf + """ + template_mgr = ctx.obj['templates'] + config = ctx.obj['config'] + + templates = template_mgr.list_templates() + + if not templates: + rprint("[yellow]⚠️ Keine Templates gefunden[/yellow]") + rprint("Verwende 'dotfiles init-templates' um Beispiel-Templates zu erstellen") + return + + # Erstelle Tabelle + table = Table(title=f"Templates für {config.current_machine}") + table.add_column("Kategorie", style="cyan") + table.add_column("Template", style="green") + table.add_column("Typ", style="yellow") + table.add_column("Größe", justify="right") + + for template in templates: + template_type = "🎨 Jinja2" if template['is_template'] else "📄 Statisch" + size = f"{template['size']} B" + + table.add_row( + template['category'], + template['name'], + template_type, + size + ) + + console.print(table) + + +@cli.command() +@click.pass_context +def init_templates(ctx): + """ + 🎨 Erstellt Beispiel-Templates + + Generiert Standard-Templates für bash, git, vim etc. + """ + rprint("[blue]🎨 Erstelle Beispiel-Templates...[/blue]") + create_example_templates() + + +@cli.command() +@click.argument('template_path') +@click.pass_context +def render(ctx, template_path): + """ + 🖨️ Rendert ein Template und zeigt das Ergebnis + + TEMPLATE_PATH: Pfad zum Template (z.B. shell/bashrc.j2) + """ + template_mgr = ctx.obj['templates'] + + try: + content = template_mgr.render_template(template_path) + rprint(f"[green]📄 Gerendert: {template_path}[/green]\n") + print(content) + + except Exception as e: + rprint(f"[red]❌ Fehler beim Rendern: {e}[/red]") + sys.exit(1) + + +@cli.command() +@click.argument('source_file', type=click.Path(exists=True)) +@click.argument('template_path') +@click.option('--extract-vars', is_flag=True, default=True, + help='Extrahiere automatisch Variablen') +@click.pass_context +def create_template(ctx, source_file, template_path, extract_vars): + """ + ➕ Erstellt ein Template aus einer existierenden Datei + + SOURCE_FILE: Pfad zur Quelldatei + TEMPLATE_PATH: Ziel-Template-Pfad (z.B. shell/bashrc.j2) + """ + template_mgr = ctx.obj['templates'] + + source_path = Path(source_file) + + if not template_path.endswith('.j2'): + template_path += '.j2' + + try: + template_mgr.create_template_from_file( + source_path, + template_path, + extract_variables=extract_vars + ) + + except Exception as e: + rprint(f"[red]❌ Fehler beim Erstellen: {e}[/red]") + sys.exit(1) + + +@cli.command() +@click.pass_context +def variables(ctx): + """ + 🔧 Zeigt alle verfügbaren Template-Variablen + """ + config = ctx.obj['config'] + variables = config.get_template_variables() + + table = Table(title="Template-Variablen") + table.add_column("Variable", style="cyan") + table.add_column("Wert", style="green") + table.add_column("Typ", style="yellow") + + for key, value in variables.items(): + table.add_row( + f"{{{{ {key} }}}}", + str(value), + type(value).__name__ + ) + + console.print(table) + + +@cli.command() +@click.pass_context +def info(ctx): + """ + ℹ️ Zeigt System-Informationen + """ + config = ctx.obj['config'] + + rprint("[bold blue]🐍 Dotfiles Management System v2.0[/bold blue]\n") + + # Aktuelle Maschine + if config.current_machine: + machine_config = config.get_machine_config() + rprint(f"[green]🖥️ Maschine:[/green] {config.current_machine}") + rprint(f"[green]📝 Beschreibung:[/green] {machine_config.description}") + else: + rprint("[red]❌ Keine Maschine erkannt[/red]") + + # Template-Statistiken + template_mgr = ctx.obj['templates'] + templates = template_mgr.list_templates() + j2_templates = [t for t in templates if t['is_template']] + static_files = [t for t in templates if not t['is_template']] + + rprint(f"\n[cyan]📊 Repository-Statistiken:[/cyan]") + rprint(f" 🎨 Jinja2-Templates: {len(j2_templates)}") + rprint(f" 📄 Statische Dateien: {len(static_files)}") + rprint(f" 📁 Gesamt: {len(templates)}") + + # Verfügbare Maschinen + rprint(f"\n[cyan]🖥️ Verfügbare Maschinen:[/cyan]") + for name, machine in config.machines.items(): + status = "✅ Aktiv" if name == config.current_machine else "⚪ Verfügbar" + rprint(f" {status} {name}: {machine.description}") + + # Konfiguration + rprint(f"\n[cyan]⚙️ Konfiguration:[/cyan]") + rprint(f" 📄 Config-Datei: {config.config_path}") + rprint(f" 🔐 Verschlüsselung: {'✅ Aktiviert' if config.is_encryption_enabled() else '❌ Deaktiviert'}") + + backup_settings = config.get_backup_settings() + rprint(f" 💾 Backups: {'✅ Aktiviert' if backup_settings.get('enabled') else '❌ Deaktiviert'}") + + +@cli.command() +@click.argument('machine_name') +@click.pass_context +def set_machine(ctx, machine_name): + """ + 🖥️ Setzt die aktuelle Maschine + + MACHINE_NAME: Name der Maschine (desktop, laptop, server) + """ + config = ctx.obj['config'] + + if machine_name not in config.machines: + rprint(f"[red]❌ Unbekannte Maschine: {machine_name}[/red]") + rprint("Verfügbare Maschinen:") + for name, machine in config.machines.items(): + rprint(f" - {name}: {machine.description}") + sys.exit(1) + + config.save_current_machine(machine_name) + config.current_machine = machine_name + + machine_config = config.get_machine_config(machine_name) + rprint(f"[green]✅ Maschine gesetzt: {machine_name}[/green]") + rprint(f"[green]📝 Beschreibung: {machine_config.description}[/green]") + + +if __name__ == '__main__': + cli() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a7a54b9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +PyYAML>=6.0 +Jinja2>=3.0.0 +click>=8.0.0 +rich>=13.0.0 +cryptography>=3.4.0 +pathspec>=0.9.0 \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..334973e --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,9 @@ +""" +Dotfiles Management System + +Ein modernes, Python-basiertes System zur Verwaltung von Konfigurationsdateien +ohne Symlinks - mit echter Datei-Synchronisation und Template-System. +""" + +__version__ = "2.0.0" +__author__ = "Your Name" \ No newline at end of file diff --git a/src/__pycache__/__init__.cpython-313.pyc b/src/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f6c4f13b742cc4b5640e4f5636f5318cac8daee9 GIT binary patch literal 404 zcmXv~u};G<5KUSN#Z`f+V)R;xCWRR>l?hQH1tBq6Cb=Z0v0d3tQ98j#u#2T znP``a0xZ4cif14}IT}^gfh6QyC@OUVMu9z|SCz}P8r^X#z&K?5w$Wv2=na?$Ubs?a zbfFc!&?*&KX}A+w*@Qa~poq>DAW0TN%`6>JDIA43cVOtGX&FV8igTltup1&RRYI>p z&I?2lY(xcQ**OWu!FaiQt8wv^OPCJZ^$uf+jv1?aj4i-e+=~TajF+g|F!t)L8!zhP zFxL{o*g}=2_>VlC8+{Kmhi@CzHnb)V852r4#)5fOcTdrTvVb$w#R-k2c7VsK?fd?B VtK)C2_PqVUWACGX_(aS$LO<+ufGq$3 literal 0 HcmV?d00001 diff --git a/src/__pycache__/config.cpython-313.pyc b/src/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d1ad62851cf93d3705771491236fdcb9ea88c2ee GIT binary patch literal 12016 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 literal 0 HcmV?d00001 diff --git a/src/__pycache__/sync.cpython-313.pyc b/src/__pycache__/sync.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..97344adb63770a0658fadf81df28e63d730f2422 GIT binary patch literal 20330 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 literal 0 HcmV?d00001 diff --git a/src/__pycache__/templates.cpython-313.pyc b/src/__pycache__/templates.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8355db6ec96432835fa56d3ebfdfebfaae145737 GIT binary patch literal 11134 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 literal 0 HcmV?d00001 diff --git a/src/config.py b/src/config.py new file mode 100644 index 0000000..bf7e705 --- /dev/null +++ b/src/config.py @@ -0,0 +1,231 @@ +""" +Konfiguration-Management für Dotfiles System +""" + +import os +import yaml +from pathlib import Path +from typing import Dict, List, Any, Optional +from dataclasses import dataclass + + +@dataclass +class MachineConfig: + """Konfiguration für einen Maschinen-Typ""" + name: str + hostname_patterns: List[str] + os_patterns: List[str] + description: str + + +@dataclass +class FileMapping: + """Mapping zwischen Template und Ziel-Datei""" + template: str + destination: str + description: str + mode: Optional[str] = None + machine_specific: bool = False + encrypted: bool = False + + +class ConfigManager: + """Verwaltet die Dotfiles-Konfiguration""" + + def __init__(self, config_path: Optional[Path] = None): + if config_path is None: + config_path = Path(__file__).parent.parent / "config.yaml" + + self.config_path = Path(config_path) + self.config_data: Dict[str, Any] = {} + self.machines: Dict[str, MachineConfig] = {} + self.file_mappings: List[FileMapping] = [] + self.current_machine: Optional[str] = None + + self.load_config() + + def load_config(self) -> None: + """Lädt die Konfiguration aus der YAML-Datei""" + try: + with open(self.config_path, 'r', encoding='utf-8') as f: + self.config_data = yaml.safe_load(f) + + self._parse_machines() + self._parse_file_mappings() + self._detect_current_machine() + + except FileNotFoundError: + raise FileNotFoundError(f"Konfigurationsdatei nicht gefunden: {self.config_path}") + except yaml.YAMLError as e: + raise ValueError(f"Fehler beim Parsen der YAML-Datei: {e}") + + def _parse_machines(self) -> None: + """Parst die Maschinen-Definitionen""" + machines_data = self.config_data.get('machines', {}) + + for name, data in machines_data.items(): + self.machines[name] = MachineConfig( + name=name, + hostname_patterns=data.get('hostname_patterns', []), + os_patterns=data.get('os_patterns', []), + description=data.get('description', '') + ) + + def _parse_file_mappings(self) -> None: + """Parst die Datei-Mappings""" + self.file_mappings = [] + + # Templates (geteilte Konfigurationen) + templates = self.config_data.get('templates', {}) + for category, files in templates.items(): + if isinstance(files, list): + for file_config in files: + mapping = FileMapping( + template=file_config['template'], + destination=file_config['destination'], + description=file_config.get('description', ''), + mode=file_config.get('mode'), + machine_specific=file_config.get('machine_specific', False), + encrypted=file_config.get('encrypted', False) + ) + self.file_mappings.append(mapping) + + def _detect_current_machine(self) -> None: + """Erkennt die aktuelle Maschine automatisch""" + import socket + import platform + + # Erst aus config.yaml prüfen + saved_machine = self.config_data.get('current_machine') + if saved_machine and saved_machine in self.machines: + self.current_machine = saved_machine + return + + hostname = socket.gethostname().lower() + os_name = platform.system() + + # Automatische Erkennung + for machine_name, machine in self.machines.items(): + # Prüfe Hostname-Patterns + for pattern in machine.hostname_patterns: + if pattern.lower() in hostname: + self.current_machine = machine_name + self.save_current_machine(machine_name) + return + + # Fallback: Interaktive Auswahl + self.current_machine = self._interactive_machine_selection() + if self.current_machine: + self.save_current_machine(self.current_machine) + + def _interactive_machine_selection(self) -> Optional[str]: + """Interaktive Maschinen-Auswahl""" + print("\n🤖 Automatische Maschinen-Erkennung fehlgeschlagen") + print("Bitte wähle deinen Maschinen-Typ:\n") + + machine_list = list(self.machines.keys()) + for i, (name, machine) in enumerate(self.machines.items(), 1): + print(f"{i}) {machine.description}") + + while True: + try: + choice = input(f"\nEingabe (1-{len(machine_list)}): ").strip() + index = int(choice) - 1 + if 0 <= index < len(machine_list): + return machine_list[index] + else: + print("❌ Ungültige Auswahl!") + except (ValueError, KeyboardInterrupt): + print("❌ Ungültige Eingabe!") + return None + + def save_current_machine(self, machine_name: str) -> None: + """Speichert die aktuelle Maschine in der config.yaml""" + self.config_data['current_machine'] = machine_name + + try: + with open(self.config_path, 'w', encoding='utf-8') as f: + yaml.dump(self.config_data, f, default_flow_style=False, + allow_unicode=True, indent=2) + except Exception as e: + print(f"⚠️ Warnung: Konnte Maschinen-Konfiguration nicht speichern: {e}") + + def get_machine_config(self, machine_name: Optional[str] = None) -> Optional[MachineConfig]: + """Gibt die Konfiguration für eine Maschine zurück""" + if machine_name is None: + machine_name = self.current_machine + + return self.machines.get(machine_name) if machine_name else None + + def get_templates_for_machine(self, machine_name: Optional[str] = None) -> List[FileMapping]: + """Gibt alle Templates für eine Maschine zurück""" + if machine_name is None: + machine_name = self.current_machine + + result = [] + for mapping in self.file_mappings: + # Alle nicht-maschinenspezifischen Templates + if not mapping.machine_specific: + result.append(mapping) + # Maschinenspezifische Templates nur für die aktuelle Maschine + elif mapping.machine_specific and machine_name: + # Template-Pfad für aktuelle Maschine anpassen + machine_template = mapping.template.replace('{machine}', machine_name) + machine_mapping = FileMapping( + template=machine_template, + destination=mapping.destination, + description=mapping.description, + mode=mapping.mode, + machine_specific=True, + encrypted=mapping.encrypted + ) + result.append(machine_mapping) + + return result + + def get_template_variables(self) -> Dict[str, Any]: + """Gibt Template-Variablen zurück""" + import getpass + import socket + + variables = { + 'machine': self.current_machine, + 'hostname': socket.gethostname(), + 'username': getpass.getuser(), + 'home': str(Path.home()), + } + + # Benutzerdefinierte Variablen aus config.yaml + user_vars = self.config_data.get('variables', {}) + variables.update(user_vars) + + # Maschinenspezifische Variablen + if self.current_machine: + machine_vars = self.config_data.get('machine_variables', {}).get(self.current_machine, {}) + variables.update(machine_vars) + + return variables + + def get_encryption_patterns(self) -> List[str]: + """Gibt Muster für zu verschlüsselnde Dateien zurück""" + return self.config_data.get('encryption', {}).get('patterns', []) + + def is_encryption_enabled(self) -> bool: + """Prüft ob Verschlüsselung aktiviert ist""" + return self.config_data.get('encryption', {}).get('enabled', False) + + def get_backup_settings(self) -> Dict[str, Any]: + """Gibt Backup-Einstellungen zurück""" + return self.config_data.get('backup', { + 'enabled': True, + 'format': '.backup.{timestamp}', + 'keep_backups': 5 + }) + + def get_sync_settings(self) -> Dict[str, Any]: + """Gibt Synchronisation-Einstellungen zurück""" + return self.config_data.get('sync', { + 'dry_run': False, + 'interactive': True, + 'force_overwrite': False + }) \ No newline at end of file diff --git a/src/sync.py b/src/sync.py new file mode 100644 index 0000000..10c7682 --- /dev/null +++ b/src/sync.py @@ -0,0 +1,397 @@ +""" +Datei-Synchronisation ohne Symlinks +""" + +import os +import shutil +import hashlib +import subprocess +from pathlib import Path +from typing import Dict, List, Optional, Tuple +from datetime import datetime +import difflib + +from .config import ConfigManager, FileMapping + + +class SyncResult: + """Ergebnis einer Synchronisation""" + def __init__(self): + self.copied: List[str] = [] + self.skipped: List[str] = [] + self.backed_up: List[str] = [] + self.conflicts: List[str] = [] + self.errors: List[str] = [] + + +class FileSynchronizer: + """Synchronisiert Dateien zwischen Templates und Home-Verzeichnis""" + + def __init__(self, config_manager: ConfigManager): + self.config = config_manager + self.home_dir = Path.home() + self.dotfiles_dir = Path(__file__).parent.parent + self.templates_dir = self.dotfiles_dir / "templates" + + def install(self, dry_run: bool = False, force: bool = False, interactive: bool = True) -> SyncResult: + """Installiert Templates ins Home-Verzeichnis""" + result = SyncResult() + + templates = self.config.get_templates_for_machine() + + print(f"🔄 Installiere {len(templates)} Konfigurationsdateien...") + if dry_run: + print("🧪 DRY RUN - Keine Dateien werden verändert") + + for mapping in templates: + try: + self._sync_template_to_home(mapping, result, dry_run, force, interactive) + except Exception as e: + result.errors.append(f"{mapping.description}: {e}") + print(f"❌ Fehler bei {mapping.description}: {e}") + + self._print_sync_summary(result, "Installation") + return result + + def backup(self, dry_run: bool = False, git_push: bool = False) -> SyncResult: + """Sichert Dateien vom Home-Verzeichnis ins Repository""" + result = SyncResult() + + templates = self.config.get_templates_for_machine() + + print(f"💾 Sichere {len(templates)} Konfigurationsdateien...") + if dry_run: + print("🧪 DRY RUN - Keine Dateien werden verändert") + + for mapping in templates: + try: + self._sync_home_to_template(mapping, result, dry_run) + except Exception as e: + result.errors.append(f"{mapping.description}: {e}") + print(f"❌ Fehler bei {mapping.description}: {e}") + + self._print_sync_summary(result, "Backup") + + # Git-Push wenn gewünscht und Dateien kopiert wurden + if git_push and not dry_run and (result.copied or result.backed_up): + try: + self._git_push_changes(result) + except Exception as e: + result.errors.append(f"Git-Push: {e}") + print(f"❌ Git-Push Fehler: {e}") + + return result + + def _sync_template_to_home(self, mapping: FileMapping, result: SyncResult, + dry_run: bool, force: bool, interactive: bool) -> None: + """Synchronisiert eine Template-Datei ins Home-Verzeichnis""" + + # Template-Pfad + template_path = self.templates_dir / mapping.template + if not template_path.exists(): + result.skipped.append(f"{mapping.description}: Template nicht gefunden") + print(f"⚠️ Template nicht gefunden: {template_path}") + return + + # Ziel-Pfad expandieren + dest_path = self._expand_path(mapping.destination) + + # Template rendern + if template_path.suffix == '.j2': + content = self._render_template(template_path) + needs_copy = self._content_differs_from_file(content, dest_path) + else: + needs_copy = self._files_differ(template_path, dest_path) + content = None + + # Prüfe ob Kopieren nötig ist + if not needs_copy: + result.skipped.append(f"{mapping.description}: Bereits aktuell") + print(f"✅ {mapping.description}: Bereits aktuell") + return + + # Handle Konflikte + if dest_path.exists() and not force: + if interactive: + action = self._handle_conflict(template_path, dest_path, mapping.description, content) + if action == 'skip': + result.skipped.append(f"{mapping.description}: Vom Benutzer übersprungen") + return + elif action == 'backup': + self._create_backup(dest_path, result) + else: + # Im nicht-interaktiven Modus: Backup erstellen + self._create_backup(dest_path, result) + + if dry_run: + result.copied.append(f"{mapping.description}: Würde kopiert werden") + print(f"🔄 {mapping.description}: Würde kopiert werden") + return + + # Erstelle Zielverzeichnis + dest_path.parent.mkdir(parents=True, exist_ok=True) + + # Kopiere Datei + if content is not None: + # Template wurde gerendert + dest_path.write_text(content, encoding='utf-8') + else: + # Normale Datei kopieren + shutil.copy2(template_path, dest_path) + + # Setze Berechtigungen + if mapping.mode: + os.chmod(dest_path, int(mapping.mode, 8)) + + result.copied.append(mapping.description) + print(f"✅ {mapping.description}: Installiert") + + def _sync_home_to_template(self, mapping: FileMapping, result: SyncResult, dry_run: bool) -> None: + """Synchronisiert eine Datei vom Home-Verzeichnis ins Repository""" + + # Quell-Pfad expandieren + source_path = self._expand_path(mapping.destination) + if not source_path.exists(): + result.skipped.append(f"{mapping.description}: Datei existiert nicht") + print(f"⚠️ {mapping.description}: Datei existiert nicht im Home-Verzeichnis") + return + + # Template-Pfad (ohne .j2 Endung für Backup) + template_path = self.templates_dir / mapping.template + if template_path.suffix == '.j2': + template_path = template_path.with_suffix('') + + # Prüfe ob Kopieren nötig ist + if self._files_differ(source_path, template_path): + if dry_run: + result.copied.append(f"{mapping.description}: Würde gesichert werden") + print(f"💾 {mapping.description}: Würde gesichert werden") + return + + # Erstelle Template-Verzeichnis + template_path.parent.mkdir(parents=True, exist_ok=True) + + # Kopiere Datei + shutil.copy2(source_path, template_path) + + result.copied.append(mapping.description) + print(f"✅ {mapping.description}: Gesichert") + else: + result.skipped.append(f"{mapping.description}: Bereits aktuell") + print(f"✅ {mapping.description}: Bereits aktuell") + + def _render_template(self, template_path: Path) -> str: + """Rendert ein Jinja2-Template""" + from jinja2 import Template + + template_content = template_path.read_text(encoding='utf-8') + template = Template(template_content) + + variables = self.config.get_template_variables() + return template.render(**variables) + + def _expand_path(self, path_str: str) -> Path: + """Expandiert einen Pfad-String mit Variablen""" + # ~ expandieren + if path_str.startswith('~'): + path_str = str(self.home_dir) + path_str[1:] + + # Variablen ersetzen + variables = self.config.get_template_variables() + for key, value in variables.items(): + path_str = path_str.replace(f'{{{key}}}', str(value)) + + return Path(path_str) + + def _files_differ(self, file1: Path, file2: Path) -> bool: + """Prüft ob zwei Dateien unterschiedlich sind""" + if not file1.exists() or not file2.exists(): + return True + + return self._get_file_hash(file1) != self._get_file_hash(file2) + + def _content_differs_from_file(self, content: str, file_path: Path) -> bool: + """Prüft ob Inhalt von einer Datei abweicht""" + if not file_path.exists(): + return True + + existing_content = file_path.read_text(encoding='utf-8') + return content != existing_content + + def _get_file_hash(self, file_path: Path) -> str: + """Berechnet SHA256-Hash einer Datei""" + hash_sha256 = hashlib.sha256() + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_sha256.update(chunk) + return hash_sha256.hexdigest() + + def _create_backup(self, file_path: Path, result: SyncResult) -> None: + """Erstellt ein Backup einer Datei""" + backup_settings = self.config.get_backup_settings() + + if not backup_settings.get('enabled', True): + return + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + backup_format = backup_settings.get('format', '.backup.{timestamp}') + backup_suffix = backup_format.format(timestamp=timestamp) + + backup_path = file_path.with_name(file_path.name + backup_suffix) + + shutil.copy2(file_path, backup_path) + result.backed_up.append(str(backup_path)) + print(f"💾 Backup erstellt: {backup_path.name}") + + def _handle_conflict(self, template_path: Path, dest_path: Path, + description: str, template_content: Optional[str] = None) -> str: + """Behandelt Konflikte zwischen Template und existierender Datei""" + + print(f"\n⚠️ Konflikt bei {description}") + print(f" Template: {template_path}") + print(f" Ziel: {dest_path}") + + # Zeige Unterschiede + if template_content: + existing_content = dest_path.read_text(encoding='utf-8') + template_lines = template_content.splitlines(keepends=True) + existing_lines = existing_content.splitlines(keepends=True) + else: + template_lines = template_path.read_text(encoding='utf-8').splitlines(keepends=True) + existing_lines = dest_path.read_text(encoding='utf-8').splitlines(keepends=True) + + diff = list(difflib.unified_diff( + existing_lines, template_lines, + fromfile=f'aktuell ({dest_path.name})', + tofile=f'template ({template_path.name})', + lineterm='' + )) + + if diff: + print("\n📝 Unterschiede:") + for line in diff[:20]: # Begrenzt auf 20 Zeilen + print(f" {line}") + if len(diff) > 20: + print(f" ... und {len(diff) - 20} weitere Zeilen") + + print("\nWas möchtest du tun?") + print("1) Überschreiben (mit Backup)") + print("2) Überspringen") + print("3) Diff zeigen") + + while True: + choice = input("Eingabe (1-3): ").strip() + + if choice == '1': + return 'backup' + elif choice == '2': + return 'skip' + elif choice == '3': + # Zeige vollständiges Diff + for line in diff: + print(line) + print("\nWas möchtest du tun?") + print("1) Überschreiben (mit Backup)") + print("2) Überspringen") + continue + else: + print("❌ Ungültige Eingabe!") + + def _print_sync_summary(self, result: SyncResult, operation: str) -> None: + """Gibt eine Zusammenfassung der Synchronisation aus""" + print(f"\n📊 {operation} abgeschlossen:") + + if result.copied: + print(f"✅ {len(result.copied)} Dateien verarbeitet") + + if result.skipped: + print(f"⏭️ {len(result.skipped)} Dateien übersprungen") + + if result.backed_up: + print(f"💾 {len(result.backed_up)} Backups erstellt") + + if result.conflicts: + print(f"⚠️ {len(result.conflicts)} Konflikte") + + if result.errors: + print(f"❌ {len(result.errors)} Fehler") + for error in result.errors: + print(f" {error}") + + def status(self) -> None: + """Zeigt den Status aller Konfigurationsdateien""" + templates = self.config.get_templates_for_machine() + + print(f"📋 Status von {len(templates)} Konfigurationsdateien:\n") + + for mapping in templates: + template_path = self.templates_dir / mapping.template + dest_path = self._expand_path(mapping.destination) + + # Status ermitteln + if not template_path.exists(): + status = "❓ Template fehlt" + elif not dest_path.exists(): + status = "➕ Nicht installiert" + elif template_path.suffix == '.j2': + # Template-Datei + content = self._render_template(template_path) + if self._content_differs_from_file(content, dest_path): + status = "🔄 Unterschiede" + else: + status = "✅ Aktuell" + else: + # Normale Datei + if self._files_differ(template_path, dest_path): + status = "🔄 Unterschiede" + else: + status = "✅ Aktuell" + + print(f"{status} {mapping.description}") + print(f" Template: {template_path}") + print(f" Ziel: {dest_path}") + print() + + def _git_push_changes(self, result: SyncResult) -> None: + """Führt Git-Operationen nach erfolgreichem Backup durch""" + + print("\n🔄 Führe Git-Operationen durch...") + + # Prüfe ob wir in einem Git-Repository sind + if not (self.dotfiles_dir / '.git').exists(): + raise Exception("Nicht in einem Git-Repository") + + # Wechsle ins dotfiles-Verzeichnis + os.chdir(self.dotfiles_dir) + + # Git status prüfen + result_status = subprocess.run(['git', 'status', '--porcelain'], + capture_output=True, text=True, check=True) + + if not result_status.stdout.strip(): + print("✅ Keine Git-Änderungen zu committen") + return + + # Git add . + print("📝 Füge Änderungen zu Git hinzu...") + subprocess.run(['git', 'add', '.'], check=True) + + # Git commit mit automatischer Message + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + machine = self.config.current_machine or 'unknown' + + changed_configs = len(result.copied) + len(result.backed_up) + commit_msg = f"Backup: {changed_configs} configs von {machine} - {timestamp}" + + print(f"💬 Erstelle Commit: {commit_msg}") + subprocess.run(['git', 'commit', '-m', commit_msg], check=True) + + # Git push + print("🚀 Pushe ins Remote-Repository...") + result_push = subprocess.run(['git', 'push'], + capture_output=True, text=True, check=True) + + if result_push.returncode == 0: + print("✅ Erfolgreich ins Git-Repository gepusht!") + else: + raise Exception(f"Git push fehlgeschlagen: {result_push.stderr}") \ No newline at end of file diff --git a/src/templates.py b/src/templates.py new file mode 100644 index 0000000..89efa44 --- /dev/null +++ b/src/templates.py @@ -0,0 +1,318 @@ +""" +Template-System für dynamische Konfigurationsdateien +""" + +import os +from pathlib import Path +from typing import Dict, Any, List +from jinja2 import Environment, FileSystemLoader, Template + +from .config import ConfigManager + + +class TemplateManager: + """Verwaltet Templates für Konfigurationsdateien""" + + def __init__(self, config_manager: ConfigManager): + self.config = config_manager + self.dotfiles_dir = Path(__file__).parent.parent + self.templates_dir = self.dotfiles_dir / "templates" + + # Jinja2 Environment einrichten + self.env = Environment( + loader=FileSystemLoader(str(self.templates_dir)), + trim_blocks=True, + lstrip_blocks=True, + keep_trailing_newline=True + ) + + # Custom Filters hinzufügen + self.env.filters['home'] = self._filter_home + self.env.filters['machine'] = self._filter_machine + + def render_template(self, template_path: str, variables: Dict[str, Any] = None) -> str: + """Rendert ein Template mit den gegebenen Variablen""" + if variables is None: + variables = self.config.get_template_variables() + + template = self.env.get_template(template_path) + return template.render(**variables) + + def create_template_from_file(self, source_file: Path, template_path: str, + extract_variables: bool = True) -> None: + """Erstellt ein Template aus einer existierenden Datei""" + + # Lese existierende Datei + content = source_file.read_text(encoding='utf-8') + + if extract_variables: + # Automatische Variablen-Extraktion + content = self._extract_common_variables(content) + + # Erstelle Template-Verzeichnis + template_file = self.templates_dir / template_path + template_file.parent.mkdir(parents=True, exist_ok=True) + + # Schreibe Template + template_file.write_text(content, encoding='utf-8') + + print(f"✅ Template erstellt: {template_file}") + + def _extract_common_variables(self, content: str) -> str: + """Extrahiert häufige Variablen aus dem Inhalt""" + variables = self.config.get_template_variables() + + # Ersetze bekannte Werte durch Template-Variablen + for var_name, var_value in variables.items(): + if isinstance(var_value, str) and var_value in content: + content = content.replace(var_value, f'{{{{ {var_name} }}}}') + + return content + + def _filter_home(self, path: str) -> str: + """Jinja2 Filter: Expandiert ~ zu Home-Verzeichnis""" + if path.startswith('~'): + return str(Path.home()) + path[1:] + return path + + def _filter_machine(self, template_dict: Dict[str, str]) -> str: + """Jinja2 Filter: Wählt Wert basierend auf aktueller Maschine""" + machine = self.config.current_machine + return template_dict.get(machine, template_dict.get('default', '')) + + def list_templates(self) -> List[Dict[str, Any]]: + """Listet alle verfügbaren Templates auf""" + templates = [] + + for template_file in self.templates_dir.rglob('*'): + if template_file.is_file(): + rel_path = template_file.relative_to(self.templates_dir) + + templates.append({ + 'path': str(rel_path), + 'name': template_file.name, + 'category': rel_path.parts[0] if len(rel_path.parts) > 1 else 'root', + 'is_template': template_file.suffix == '.j2', + 'size': template_file.stat().st_size, + 'modified': template_file.stat().st_mtime + }) + + return sorted(templates, key=lambda t: t['path']) + + def validate_template(self, template_path: str) -> List[str]: + """Validiert ein Template und gibt eventuelle Fehler zurück""" + errors = [] + + try: + template = self.env.get_template(template_path) + + # Versuche zu rendern mit Standard-Variablen + variables = self.config.get_template_variables() + template.render(**variables) + + except Exception as e: + errors.append(f"Template-Fehler: {e}") + + return errors + + def get_template_variables_usage(self, template_path: str) -> List[str]: + """Gibt alle in einem Template verwendeten Variablen zurück""" + try: + template = self.env.get_template(template_path) + + # Extrahiere verwendete Variablen (vereinfacht) + from jinja2 import meta + ast = self.env.parse(template.source) + variables = meta.find_undeclared_variables(ast) + + return sorted(list(variables)) + + except Exception: + return [] + + +def create_example_templates(): + """Erstellt Beispiel-Templates""" + + templates_dir = Path(__file__).parent.parent / "templates" + + # Shell-Templates + shell_dir = templates_dir / "shell" + shell_dir.mkdir(parents=True, exist_ok=True) + + # Bashrc Template + bashrc_template = shell_dir / "bashrc.j2" + bashrc_content = '''# Bash configuration for {{ machine }} ({{ hostname }}) +# Generated by dotfiles system + +# History settings +HISTSIZE=10000 +HISTFILESIZE=20000 +HISTCONTROL=ignoreboth + +# Aliases +alias ll='ls -alF' +alias la='ls -A' +alias l='ls -CF' +alias grep='grep --color=auto' + +{% if machine == 'server' %} +# Server-specific settings +alias logs='sudo journalctl -f' +alias status='systemctl status' +{% elif machine == 'desktop' %} +# Desktop-specific settings +alias open='xdg-open' +alias screenshot='gnome-screenshot' +{% endif %} + +# User-specific settings +export EDITOR=vim +export PATH="$HOME/.local/bin:$PATH" + +# Machine-specific additions +{% if machine == 'laptop' %} +# Laptop power settings +alias hibernate='systemctl hibernate' +alias suspend='systemctl suspend' +{% endif %} + +# Load local bashrc if it exists +if [ -f ~/.bashrc.local ]; then + source ~/.bashrc.local +fi +''' + bashrc_template.write_text(bashrc_content, encoding='utf-8') + + # Git Template + git_dir = templates_dir / "git" + git_dir.mkdir(parents=True, exist_ok=True) + + gitconfig_template = git_dir / "gitconfig.j2" + gitconfig_content = '''[user] + name = {{ git_name | default("Your Name") }} + email = {{ git_email | default("your.email@example.com") }} + +[core] + editor = {{ editor | default("vim") }} + autocrlf = input + excludesfile = ~/.gitignore_global + +[push] + default = simple + +[pull] + rebase = false + +[alias] + st = status + co = checkout + br = branch + ci = commit + lg = log --oneline --graph --decorate + unstage = reset HEAD -- + +{% if machine == 'desktop' %} +[diff] + tool = meld +{% endif %} + +[color] + ui = auto + branch = auto + diff = auto + status = auto + +[color "branch"] + current = yellow reverse + local = yellow + remote = green + +[color "diff"] + meta = yellow bold + frag = magenta bold + old = red bold + new = green bold + +[color "status"] + added = yellow + changed = green + untracked = cyan +''' + gitconfig_template.write_text(gitconfig_content, encoding='utf-8') + + # Vim Template + vim_dir = templates_dir / "vim" + vim_dir.mkdir(parents=True, exist_ok=True) + + vimrc_template = vim_dir / "vimrc.j2" + vimrc_content = '''" Vim configuration for {{ machine }} +" Generated by dotfiles system + +" Basic settings +set nocompatible +set number +set ruler +set showcmd +set incsearch +set hlsearch + +" Indentation +set autoindent +set smartindent +set tabstop=4 +set shiftwidth=4 +set expandtab + +" Colors and theme +syntax enable +set background=dark + +{% if machine == 'desktop' %} +" Desktop-specific settings +set mouse=a +set clipboard=unnamedplus +{% endif %} + +" File handling +set encoding=utf-8 +set fileencoding=utf-8 +set backspace=indent,eol,start + +" Backup settings +set nobackup +set nowritebackup +set noswapfile + +" Search settings +set ignorecase +set smartcase + +" Key mappings +let mapleader = "," +nnoremap w :w +nnoremap q :q + +{% if machine == 'server' %} +" Server-specific: lighter config +set laststatus=1 +{% else %} +" Desktop/Laptop: enhanced features +set laststatus=2 +set wildmenu +set wildmode=list:longest +{% endif %} + +" Load local vimrc if exists +if filereadable(expand("~/.vimrc.local")) + source ~/.vimrc.local +endif +''' + vimrc_template.write_text(vimrc_content, encoding='utf-8') + + print(f"✅ Beispiel-Templates erstellt in: {templates_dir}") + + +if __name__ == "__main__": + # Erstelle Beispiel-Templates wenn direkt ausgeführt + create_example_templates() \ No newline at end of file diff --git a/templates/git/gitconfig b/templates/git/gitconfig new file mode 100644 index 0000000..7bd85bb --- /dev/null +++ b/templates/git/gitconfig @@ -0,0 +1,4 @@ +[user] + name = cseyfferth + mail = mail@seyfferth.dev + email = mail@seyfferth.dev diff --git a/templates/git/gitconfig.j2 b/templates/git/gitconfig.j2 new file mode 100644 index 0000000..9760ca5 --- /dev/null +++ b/templates/git/gitconfig.j2 @@ -0,0 +1,49 @@ +[user] + name = {{ git_name | default("Your Name") }} + email = {{ git_email | default("your.email@example.com") }} + +[core] + editor = {{ editor | default("vim") }} + autocrlf = input + excludesfile = ~/.gitignore_global + +[push] + default = simple + +[pull] + rebase = false + +[alias] + st = status + co = checkout + br = branch + ci = commit + lg = log --oneline --graph --decorate + unstage = reset HEAD -- + +{% if machine == 'desktop' %} +[diff] + tool = meld +{% endif %} + +[color] + ui = auto + branch = auto + diff = auto + status = auto + +[color "branch"] + current = yellow reverse + local = yellow + remote = green + +[color "diff"] + meta = yellow bold + frag = magenta bold + old = red bold + new = green bold + +[color "status"] + added = yellow + changed = green + untracked = cyan diff --git a/templates/shell/bashrc b/templates/shell/bashrc new file mode 100644 index 0000000..63a556e --- /dev/null +++ b/templates/shell/bashrc @@ -0,0 +1,23 @@ +OS=$(cat /etc/os-release | grep '^ID=' | cut -d "=" -f2) + + +. /etc/skel/.bashrc + +alias ls='ls --color=auto' +alias grep='grep --color=auto' +alias codium="codium --ozone-platform-hint=wayland" + +export PATH=~/.cargo/bin:~/.bin:/home/cseyfferth/.local/bin:$PATH +# PS1='[\u@\h \W]\$ ' + +export BW_SESSION="+GCKSMq51RO3R/+zNblGqegfqZnA+FnywqaNEuiakQafhtah/4jlenScb387utdOQoyhx+5AorSUOFV149zqnA==" +export SSH_AUTH_SOCK=$XDG_RUNTIME_DIR/gcr/ssh + +export PATH=~/node_modules/.bin:$PATH +export PATH=~/.bin/:$PATH + +[[ "$TERM_PROGRAM" == "vscode" ]] && . "$(code --locate-shell-integration-path bash)" + +#THIS MUST BE AT THE END OF THE FILE FOR SDKMAN TO WORK!!! +export SDKMAN_DIR="$HOME/.sdkman" +[[ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]] && source "$HOME/.sdkman/bin/sdkman-init.sh" diff --git a/templates/shell/bashrc.j2 b/templates/shell/bashrc.j2 new file mode 100644 index 0000000..5d4f0e3 --- /dev/null +++ b/templates/shell/bashrc.j2 @@ -0,0 +1,39 @@ +# Bash configuration for {{ machine }} ({{ hostname }}) +# Generated by dotfiles system + +# History settings +HISTSIZE=10000 +HISTFILESIZE=20000 +HISTCONTROL=ignoreboth + +# Aliases +alias ll='ls -alF' +alias la='ls -A' +alias l='ls -CF' +alias grep='grep --color=auto' + +{% if machine == 'server' %} +# Server-specific settings +alias logs='sudo journalctl -f' +alias status='systemctl status' +{% elif machine == 'desktop' %} +# Desktop-specific settings +alias open='xdg-open' +alias screenshot='gnome-screenshot' +{% endif %} + +# User-specific settings +export EDITOR=vim +export PATH="$HOME/.local/bin:$PATH" + +# Machine-specific additions +{% if machine == 'laptop' %} +# Laptop power settings +alias hibernate='systemctl hibernate' +alias suspend='systemctl suspend' +{% endif %} + +# Load local bashrc if it exists +if [ -f ~/.bashrc.local ]; then + source ~/.bashrc.local +fi diff --git a/templates/shell/zshrc b/templates/shell/zshrc new file mode 100644 index 0000000..b2d94cd --- /dev/null +++ b/templates/shell/zshrc @@ -0,0 +1,85 @@ + +bindkey '[3~' delete-char + +function load_venv_on_cd() { + if [[ -d "./venv" ]]; then + source "./venv/bin/activate" + echo "Python venv aktiviert: ./venv" + elif [[ -d "./.venv" ]]; then + source "./.venv/bin/activate" + echo "Python venv aktiviert: ./.venv" + fi +} + +if [[ ! "$TERM_PROGRAM" == "vscode" ]]; then + + typeset -g POWERLEVEL9K_TERM_SHELL_INTEGRATION=true + # Enable Powerlevel10k instant prompt. Should stay close to the top of ~/.zshrc. + # Initialization code that may require console input (password prompts, [y/n] + # confirmations, etc.) must go above this block; everything else may go below. + if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then + source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" + fi + + + # The following lines were added by compinstall + + zstyle ':completion:*' completer _expand _complete _ignored _correct _approximate + zstyle ':completion:*' group-name '' + zstyle :compinstall filename '/home/cseyfferth/.zshrc' + + autoload -Uz compinit + compinit + # End of lines added by compinstall + # Lines configured by zsh-newuser-install + HISTFILE=~/.histfile + HISTSIZE=1000 + SAVEHIST=1000 + # End of lines configured by zsh-newuser-install + + PROMPT="[%n@%m %~]$ " + RPROMPT='%(?.%F{green}√.%F{red}?%?)%f~' + + source /usr/share/zsh/plugins/fast-syntax-highlighting/fast-syntax-highlighting.plugin.zsh + #source /usr/share/zsh/scripts/zplug/init.zsh + source /usr/share/zsh-theme-powerlevel10k/powerlevel10k.zsh-theme + + # To customize prompt, run `p10k configure` or edit ~/.p10k.zsh. + [[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh + + zstyle ':completion:*:(ssh|scp|sftp):*:hosts' ignored-patterns '*' +# zstyle ':completion:*:ssh:*' config-file ~/.ssh/config + + zstyle ':completion:*' insert-tab false + autoload -Uz compinit + compinit + zstyle ':completion:*' menu select + + export BW_SESSION="+GCKSMq51RO3R/+zNblGqegfqZnA+FnywqaNEuiakQafhtah/4jlenScb387utdOQoyhx+5AorSUOFV149zqnA==" + export SSH_AUTH_SOCK=$XDG_RUNTIME_DIR/gcr/ssh + export PATH=/home/cseyfferth/.bin/:$PATH + +else + echo "🚀 Powerlevel10k instant prompt is disabled in Visual Studio Code terminal. To enable, set TERM_PROGRAM to a value other than 'vscode'." + [[ "$TERM_PROGRAM" == "vscode" ]] && . "$(code --locate-shell-integration-path zsh 2>/dev/null)" 2>/dev/null || . "$(code-insiders --locate-shell-integration-path zsh 2>/dev/null)" + # wenn $? = 0 dann ist der letzte Befehl erfolgreich ausgeführt worden + if [[ $? -eq 0 ]]; then + echo "🚀 Visual Studio Code shell integration enabled." + else + echo "🚀 Visual Studio Code shell integration not found. Please make sure that 'code' is in PATH." + fi + # set prompt + PROMPT="[%n@%m %~]$ " +fi + +export PATH=/home/cseyfferth/.cargo/bin:$PATH + +autoload -U add-zsh-hook +add-zsh-hook chpwd load_venv_on_cd + +#THIS MUST BE AT THE END OF THE FILE FOR SDKMAN TO WORK!!! +export SDKMAN_DIR="$HOME/.sdkman" +[[ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]] && source "$HOME/.sdkman/bin/sdkman-init.sh" + +# Created by `pipx` on 2025-06-19 10:50:20 +export PATH="$PATH:/home/cseyfferth/.local/bin" diff --git a/templates/ssh/config b/templates/ssh/config new file mode 100644 index 0000000..a6a17d9 --- /dev/null +++ b/templates/ssh/config @@ -0,0 +1,230 @@ +include ~/.ssh/docker_config +#host * +# controlmaster auto +# controlpath /tmp/ssh-%r@%h:%p + +Host jupyter + User cseyfferth + ProxyCommand websocat --binary -H='Authorization: token 364c79a34c3d48509cefe2689c991541' asyncstdio: wss://jupyter.informatik.hs-bremerhaven.de/user/cseyfferth/sshd/ + +Host bs + Hostname localhost + Port 2222 + User ubuntu + StrictHostKeyChecking no + UserKnownHostsFile /dev/null + LogLevel ERROR + +Host lima + Hostname localhost + Port 60022 + IdentityFile ~/.lima/_config/user + StrictHostKeyChecking no + UserKnownHostsFile /dev/null + LogLevel ERROR + +Host limawork + Hostname localhost + Port 40375 + IdentityFile ~/.lima/_config/user + StrictHostKeyChecking no + UserKnownHostsFile /dev/null + LogLevel ERROR + +Host midgard + Hostname 157.173.99.95 + User cseyfferth + Port 22 + +Host l-firewall + Hostname 194.94.217.73 + User l-cseyfferth + +Host diso + Hostname diso.informatik.hs-bremerhaven.de + User cseyfferth + ProxyJump hopper + +Host l-diso + Hostname diso.informatik.hs-bremerhaven.de + User l-cseyfferth + ProxyJump hopper + +Host l-brewster + Hostname 194.94.217.32 + User l-cseyfferth + ProxyJump snowden + +Host l-docker-services + Hostname 194.94.217.110 + ProxyJump snowden + User l-cseyfferth + +Host l-tanenbaum + Hostname 194.94.217.120 + User l-cseyfferth + ProxyJump snowden + +Host l-dmp-website + HostName 194.94.217.115 + User l-cseyfferth + ProxyJump snowden + Port 8080 + +Host l-dmp-nextcloud + HostName 194.94.217.117 + User l-cseyfferth + ProxyJump snowden + +Host l-hamilton + Hostname 194.94.217.85 + User l-cseyfferth + ProxyJump snowden + +Host l-hilbert + HostName 194.94.217.96 + User l-cseyfferth + ProxyJump snowden + +Host l-ecomat + Hostname ecomat.hs-bremerhaven.de + User l-cseyfferth + ProxyJump hopper + +Host jitsi-test + Hostname jitsi-test.hs-bremerhaven.de + User l-cseyfferth + ProxyJump hopper + +Host bbb-ext + Hostname 136.243.192.156 + User l-cseyfferth + ProxyJump hopper + + +Host l-noether + Hostname 194.94.217.99 + User l-cseyfferth + ProxyJump snowden + +Host labormbt + Hostname 194.94.217.75 + User l-cseyfferth + ProxyJump hopper + +Host l-bladerunner + Hostname 194.94.217.80 + User l-cseyfferth + ProxyJump snowden + +Host honeypot + Hostname 10.10.10.10 + Port 65535 + User cseyfferth + ProxyJump asgard + +Host chengisao + Hostname 194.94.217.111 + User cseyfferth + IdentityFile ~/.ssh/chengisao_cseyfferth + ProxyJump hopper + +Host weizenbaum + Hostname 194.94.217.82 + User l-cseyfferth + ProxyJump snowden + +Host demo + Hostname 194.94.217.101 + User l-cseyfferth + ProxyJump hopper + +Host mitnick + Hostname 5.161.51.130 + +Host seyfferthHopper + Hostname seyfferth.dev + ProxyJump hopper + +Host hopper + User cseyfferth + Hostname hopper.informatik.hs-bremerhaven.de + Port 8080 + +Host l-hopper + Hostname hopper.informatik.hs-bremerhaven.de + Port 8080 + User l-cseyfferth + IdentityFile ~/.ssh/id_ed25519_sk_solo2_blue + ProxyJump snowden + +Host gitea + Hostname seyfferth.dev + Port 2222 + User git + +Host rhodes + Hostname 194.94.217.72 + ProxyJump snowden + User l-cseyfferth + +Host asgard + HostName 144.91.94.63 + Port 22 + User cseyfferth + +Host l-clarke + HostName 194.94.217.105 + User l-cseyfferth + ProxyJump snowden + +Host snowden + Hostname 194.94.217.95 + User l-cseyfferth + Port 443 + +Host tangens + Hostname 194.94.217.84 + User l-cseyfferth + Port 443 + +Host hopperneu + Hostname 194.94.217.93 + Port 443 + +Host l-gitlab + Hostname 194.94.217.77 + User l-cseyfferth + ProxyJump snowden + +Host l-mockapetris + HostName 194.94.217.11 + User l-cseyfferth + ProxyJump snowden + +Host l-ritchie + Hostname 194.94.217.78 + User l-cseyfferth + ProxyJump snowden + +Host l-turing + HostName 194.94.217.97 + User l-cseyfferth + ProxyJump snowden + +Host mydocker + Hostname docker-server + User docker-cseyfferth + Port 20500 + ProxyJump hopper + # IdentityFile ~/.ssh/id_rsa_hopper + +Host vm40 + Hostname 194.94.217.83 + Port 14022 + ProxyJump hopper + +Host vm41 + Hostname 194.94.217.83 + Port 14122 + ProxyJump hopper diff --git a/templates/vim/vimrc.j2 b/templates/vim/vimrc.j2 new file mode 100644 index 0000000..8d705be --- /dev/null +++ b/templates/vim/vimrc.j2 @@ -0,0 +1,61 @@ +" Vim configuration for {{ machine }} +" Generated by dotfiles system + +" Basic settings +set nocompatible +set number +set ruler +set showcmd +set incsearch +set hlsearch + +" Indentation +set autoindent +set smartindent +set tabstop=4 +set shiftwidth=4 +set expandtab + +" Colors and theme +syntax enable +set background=dark + +{% if machine == 'desktop' %} +" Desktop-specific settings +set mouse=a +set clipboard=unnamedplus +{% endif %} + +" File handling +set encoding=utf-8 +set fileencoding=utf-8 +set backspace=indent,eol,start + +" Backup settings +set nobackup +set nowritebackup +set noswapfile + +" Search settings +set ignorecase +set smartcase + +" Key mappings +let mapleader = "," +nnoremap w :w +nnoremap q :q + +{% if machine == 'server' %} +" Server-specific: lighter config +set laststatus=1 +{% else %} +" Desktop/Laptop: enhanced features +set laststatus=2 +set wildmenu +set wildmode=list:longest +{% endif %} + +" Load local vimrc if exists +if filereadable(expand("~/.vimrc.local")) + source ~/.vimrc.local +endif