Initial commit: Python dotfiles system
This commit is contained in:
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal 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
271
README.md
Normal 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
134
config.yaml
Normal 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
309
dotfiles.py
Executable 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
6
requirements.txt
Normal 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
9
src/__init__.py
Normal 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"
|
||||
BIN
src/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
src/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/config.cpython-313.pyc
Normal file
BIN
src/__pycache__/config.cpython-313.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/sync.cpython-313.pyc
Normal file
BIN
src/__pycache__/sync.cpython-313.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/templates.cpython-313.pyc
Normal file
BIN
src/__pycache__/templates.cpython-313.pyc
Normal file
Binary file not shown.
231
src/config.py
Normal file
231
src/config.py
Normal 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
397
src/sync.py
Normal 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
318
src/templates.py
Normal 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
4
templates/git/gitconfig
Normal file
@@ -0,0 +1,4 @@
|
||||
[user]
|
||||
name = cseyfferth
|
||||
mail = mail@seyfferth.dev
|
||||
email = mail@seyfferth.dev
|
||||
49
templates/git/gitconfig.j2
Normal file
49
templates/git/gitconfig.j2
Normal 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
23
templates/shell/bashrc
Normal 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
39
templates/shell/bashrc.j2
Normal 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
85
templates/shell/zshrc
Normal 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
230
templates/ssh/config
Normal 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
61
templates/vim/vimrc.j2
Normal 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
|
||||
Reference in New Issue
Block a user