motd-assist/internal/preset/preset.go
Tronax d56659e21a
feat: add motd-assist TUI for managing dynamic MOTDs
Initial Go project built with Bubble Tea and Lip Gloss:
- Live preview and management of /etc/update-motd.d scripts
- Embedded presets (ubuntu-default, minimal, server-dashboard)
- Bundled POSIX sh style scripts (brand header, sysinfo panel, footer)
- Snapshot/restore safety around applying changes
- CLI subcommands preview, apply, restore, list-presets
- Unit tests for detect, sanitize, and apply logic
2026-08-24 14:19:28 +02:00

127 lines
3.3 KiB
Go

// Package preset definiert das Preset-Schema und das Laden/Speichern
// von Built-in- und User-Presets.
package preset
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"motd-assist/internal/assets"
)
// Entry beschreibt den Soll-Zustand eines Scripts innerhalb eines Presets.
// Die Reihenfolge im Scripts-Array bestimmt die Ziel-Reihenfolge im MOTD.
type Entry struct {
Name string `json:"name"` // Zieldateiname, z.B. "10-brand-header"
Enabled bool `json:"enabled"` // Executable-Bit gesetzt
Install string `json:"install,omitempty"` // gebündelte Vorlage, leer = vorhandenes Script
}
// Preset ist eine benannte MOTD-Konfiguration.
type Preset struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Scripts []Entry `json:"scripts"`
Source string `json:"-"` // "builtin" oder "user"
}
var safeName = regexp.MustCompile(`[^a-zA-Z0-9._-]+`)
// Dir liefert das User-Preset-Verzeichnis (XDG-konform).
func Dir() (string, error) {
if d := os.Getenv("XDG_CONFIG_HOME"); d != "" {
return filepath.Join(d, "motd-assist", "presets"), nil
}
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, ".config", "motd-assist", "presets"), nil
}
// LoadAll liefert alle Presets: Built-ins, durch User-Presets mit
// gleichem Namen überschrieben. Sortiert nach Name.
func LoadAll() ([]Preset, error) {
byName := make(map[string]Preset)
for file, content := range assets.PresetFiles() {
var p Preset
if err := json.Unmarshal([]byte(content), &p); err != nil {
return nil, fmt.Errorf("builtin preset %s: %w", file, err)
}
p.Source = "builtin"
byName[p.Name] = p
}
dir, err := Dir()
if err != nil {
return nil, err
}
entries, err := os.ReadDir(dir)
if err == nil {
for _, e := range entries {
if e.IsDir() || filepath.Ext(e.Name()) != ".json" {
continue
}
b, err := os.ReadFile(filepath.Join(dir, e.Name()))
if err != nil {
continue
}
var p Preset
if err := json.Unmarshal(b, &p); err != nil {
continue
}
p.Source = "user"
byName[p.Name] = p
}
} else if !errors.Is(err, os.ErrNotExist) {
return nil, err
}
presets := make([]Preset, 0, len(byName))
for _, p := range byName {
presets = append(presets, p)
}
sort.Slice(presets, func(i, j int) bool { return presets[i].Name < presets[j].Name })
return presets, nil
}
// SaveUser schreibt ein Preset ins User-Verzeichnis (überschreibt Built-ins
// mit gleichem Namen zur Laufzeit).
func SaveUser(p Preset) (string, error) {
if p.Name == "" {
return "", errors.New("Preset-Name fehlt")
}
dir, err := Dir()
if err != nil {
return "", err
}
if err := os.MkdirAll(dir, 0o755); err != nil {
return "", err
}
b, err := json.MarshalIndent(p, "", " ")
if err != nil {
return "", err
}
path := filepath.Join(dir, safeName.ReplaceAllString(p.Name, "-")+".json")
return path, os.WriteFile(path, append(b, '\n'), 0o644)
}
// DeleteUser entfernt ein User-Preset. Built-ins können nicht gelöscht werden.
func DeleteUser(name string) error {
dir, err := Dir()
if err != nil {
return err
}
path := filepath.Join(dir, safeName.ReplaceAllString(name, "-")+".json")
if err := os.Remove(path); err != nil {
return fmt.Errorf("nur User-Presets sind löschbar: %w", err)
}
return nil
}