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
107 lines
2.8 KiB
Go
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" }
|