motd-assist/internal/motd/preview.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

107 lines
2.8 KiB
Go

package motd
import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"motd-assist/internal/assets"
)
// scriptTimeout begrenzt die Laufzeit eines einzelnen Preview-Scripts
// (langsame Netzwerk-Scripte wie 50-motd-news blockieren sonst die TUI).
const scriptTimeout = 2 * time.Second
// PreviewScript ist ein Preview-Kandidat: entweder eine vorhandene Datei
// (Path) oder eine noch nicht installierte gebündelte Vorlage (Source).
type PreviewScript struct {
Name string
Path string
Source string
Enabled bool
}
// Render führt die aktivierten Scripte in Reihenfolge aus und liefert den
// bereinigten MOTD-Output. Fehler einzelner Scripte werden als dezente
// Hinweiszeile eingebaut statt die ganze Preview abbrechen.
func Render(scripts []PreviewScript, width int) string {
var parts []string
for _, s := range scripts {
if !s.Enabled {
continue
}
path := s.Path
if path == "" {
if s.Source == "" {
continue
}
staged, cleanup, err := stagePreview(s.Source, s.Name)
if err != nil {
parts = append(parts, dimLine(fmt.Sprintf("%s: %v", s.Name, err)))
continue
}
path = staged
defer cleanup()
}
addPart(&parts, runScript(path, width), width)
}
return strings.Join(parts, "\n\n")
}
func addPart(parts *[]string, raw string, width int) {
if text := strings.TrimSpace(Sanitize(raw, width)); text != "" {
*parts = append(*parts, text)
}
}
func runScript(path string, width int) string {
ctx, cancel := context.WithTimeout(context.Background(), scriptTimeout)
defer cancel()
cmd := exec.CommandContext(ctx, path)
cmd.Env = append(os.Environ(),
fmt.Sprintf("COLUMNS=%d", width),
"TERM=xterm-256color",
)
cmd.Dir = "/"
var buf bytes.Buffer
cmd.Stdout = &buf
cmd.Stderr = &buf
if err := cmd.Run(); err != nil {
if ctx.Err() == context.DeadlineExceeded {
return dimLine(fmt.Sprintf("%s: Timeout", filepath.Base(path)))
}
if buf.Len() == 0 {
return dimLine(fmt.Sprintf("%s: nicht ausführbar (%v)", filepath.Base(path), err))
}
// Exit-Status != 0 ist bei MOTD-Scripten üblich; Output behalten.
}
return buf.String()
}
// stagePreview kopiert eine gebündelte Vorlage in ein Temporärverzeichnis,
// damit die Live-Vorschau sie vor der Installation zeigen kann.
func stagePreview(source, name string) (string, func(), error) {
content, ok := assets.ScriptContent(source)
if !ok {
return "", nil, fmt.Errorf("unbekannte Vorlage %q", source)
}
dir, err := os.MkdirTemp("", "motd-assist-")
if err != nil {
return "", nil, err
}
path := filepath.Join(dir, name)
if err := os.WriteFile(path, []byte(content), 0o755); err != nil {
os.RemoveAll(dir)
return "", nil, err
}
return path, func() { os.RemoveAll(dir) }, nil
}
func dimLine(s string) string { return "\x1b[2m" + s + "\x1b[0m" }