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:
commit
d56659e21a
34 changed files with 3260 additions and 0 deletions
110
internal/ui/commands.go
Normal file
110
internal/ui/commands.go
Normal 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}
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue