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
This commit is contained in:
Tronax 2026-08-24 14:19:28 +02:00
commit d56659e21a
Signed by: Tronax
SSH key fingerprint: SHA256:2pKKXDZucWvaF/GzXNz0FY53EAO1YDLN80bqS+TTz/o
34 changed files with 3260 additions and 0 deletions

110
internal/ui/commands.go Normal file
View file

@ -0,0 +1,110 @@
package ui
import (
"os"
"os/exec"
"path/filepath"
tea "github.com/charmbracelet/bubbletea"
"motd-assist/internal/motd"
"motd-assist/internal/preset"
)
type discoverMsg struct {
scripts []motd.Script
host motd.HostInfo
err error
}
type presetsMsg struct {
presets []preset.Preset
err error
}
type previewMsg struct {
version int
text string
}
type applyDoneMsg struct{ err error }
func discoverCmd(root string) tea.Cmd {
return func() tea.Msg {
scripts, err := motd.Discover(root)
return discoverMsg{scripts: scripts, host: motd.DetectHost(root), err: err}
}
}
func presetsCmd() tea.Cmd {
return func() tea.Msg {
presets, err := preset.LoadAll()
return presetsMsg{presets: presets, err: err}
}
}
// previewCmd rendert die Arbeitskopie als Live-Vorschau (asynchron, damit
// langsame Scripte die TUI nicht blockieren).
func (m *Model) previewCmd() tea.Cmd {
m.previewBusy = true
m.previewVersion++
version := m.previewVersion
width := m.previewWidth()
scripts := make([]motd.PreviewScript, 0, len(m.working))
for _, r := range m.working {
if r.Remove {
continue
}
ps := motd.PreviewScript{Name: r.Name, Enabled: r.Enabled}
if r.Exists {
ps.Path = filepath.Join(motd.MotdDir(m.root), r.Name)
} else {
ps.Source = r.Install
}
scripts = append(scripts, ps)
}
return func() tea.Msg {
return previewMsg{version: version, text: motd.Render(scripts, width)}
}
}
// previewWidth ist die nutzbare Breite innerhalb der Preview-Umrandung.
func (m *Model) previewWidth() int {
w := m.width - 4 // Rahmen (2) + Padding (2)
if w < 40 {
w = 40
}
if w > 100 {
w = 100
}
return w
}
// applyExec handelt das Terminal an sudo weiter (interaktive
// Passwortabfrage) und startet danach die TUI weiter.
func applyExec(plan motd.Plan) tea.Cmd {
planFile, err := plan.WriteFile()
if err != nil {
return func() tea.Msg { return applyDoneMsg{err: err} }
}
self, err := os.Executable()
if err != nil {
os.Remove(planFile)
return func() tea.Msg { return applyDoneMsg{err: err} }
}
var cmd *exec.Cmd
if motd.IsRoot() {
cmd = exec.Command(self, "apply-state", planFile)
} else {
sudo, err := exec.LookPath("sudo")
if err != nil {
os.Remove(planFile)
return func() tea.Msg { return applyDoneMsg{err: err} }
}
cmd = exec.Command(sudo, "-p", "[motd-assist] Passwort für %p: ", self, "apply-state", planFile)
}
return tea.ExecProcess(cmd, func(err error) tea.Msg {
os.Remove(planFile)
return applyDoneMsg{err: err}
})
}