Initial commit: Python dotfiles system

This commit is contained in:
2025-07-03 17:14:52 +02:00
commit 6b4f19c2dc
20 changed files with 2185 additions and 0 deletions

19
.gitignore vendored Normal file
View File

@@ -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

271
README.md Normal file
View File

@@ -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 <your-dotfiles-repo-url> ~/.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. ✨

134
config.yaml Normal file
View File

@@ -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

309
dotfiles.py Executable file
View File

@@ -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()

6
requirements.txt Normal file
View File

@@ -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

9
src/__init__.py Normal file
View File

@@ -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"

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

231
src/config.py Normal file
View File

@@ -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
})

397
src/sync.py Normal file
View File

@@ -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}")

318
src/templates.py Normal file
View File

@@ -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 <leader>w :w<CR>
nnoremap <leader>q :q<CR>
{% 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()

4
templates/git/gitconfig Normal file
View File

@@ -0,0 +1,4 @@
[user]
name = cseyfferth
mail = mail@seyfferth.dev
email = mail@seyfferth.dev

View File

@@ -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

23
templates/shell/bashrc Normal file
View File

@@ -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"

39
templates/shell/bashrc.j2 Normal file
View File

@@ -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

85
templates/shell/zshrc Normal file
View File

@@ -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"

230
templates/ssh/config Normal file
View File

@@ -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

61
templates/vim/vimrc.j2 Normal file
View File

@@ -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 <leader>w :w<CR>
nnoremap <leader>q :q<CR>
{% 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