commit 6b4f19c2dc57c645ed3e43635bd3a0fb12c56908 Author: cseyfferth Date: Thu Jul 3 17:14:52 2025 +0200 Initial commit: Python dotfiles system 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 0000000..f6c4f13 Binary files /dev/null and b/src/__pycache__/__init__.cpython-313.pyc differ diff --git a/src/__pycache__/config.cpython-313.pyc b/src/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000..d1ad628 Binary files /dev/null and b/src/__pycache__/config.cpython-313.pyc differ diff --git a/src/__pycache__/sync.cpython-313.pyc b/src/__pycache__/sync.cpython-313.pyc new file mode 100644 index 0000000..97344ad Binary files /dev/null and b/src/__pycache__/sync.cpython-313.pyc differ diff --git a/src/__pycache__/templates.cpython-313.pyc b/src/__pycache__/templates.cpython-313.pyc new file mode 100644 index 0000000..8355db6 Binary files /dev/null and b/src/__pycache__/templates.cpython-313.pyc differ 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