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
110 lines
2.5 KiB
Go
110 lines
2.5 KiB
Go
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}
|
|
})
|
|
}
|