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
72
internal/assets/assets.go
Normal file
72
internal/assets/assets.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
// Package assets hält die eingebetteten Presets und Custom-Scripte.
|
||||
package assets
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed presets scripts
|
||||
var embedded embed.FS
|
||||
|
||||
// ScriptContent liefert den Inhalt einer gebündelten Script-Vorlage.
|
||||
func ScriptContent(name string) (string, bool) {
|
||||
b, err := embedded.ReadFile("scripts/" + name)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return string(b), true
|
||||
}
|
||||
|
||||
// ScriptNames listet alle gebündelten Script-Vorlagen.
|
||||
func ScriptNames() []string {
|
||||
entries, err := embedded.ReadDir("scripts")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
names := make([]string, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
names = append(names, e.Name())
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// PresetFiles liefert Dateiname → Inhalt aller eingebetteten Presets.
|
||||
func PresetFiles() map[string]string {
|
||||
entries, err := embedded.ReadDir("presets")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
files := make(map[string]string, len(entries))
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
if b, err := embedded.ReadFile("presets/" + e.Name()); err == nil {
|
||||
files[e.Name()] = string(b)
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
// MatchScript findet die Vorlage, deren Inhalt zu content passt, und
|
||||
// erkennt damit bereits installierte motd-assist-Scripte.
|
||||
func MatchScript(content string) (string, bool) {
|
||||
return MatchScriptBytes([]byte(content))
|
||||
}
|
||||
|
||||
// MatchScriptBytes ist die Byte-Variante von MatchScript.
|
||||
func MatchScriptBytes(b []byte) (string, bool) {
|
||||
content := strings.TrimSpace(string(b))
|
||||
if content == "" {
|
||||
return "", false
|
||||
}
|
||||
for _, name := range ScriptNames() {
|
||||
if c, ok := ScriptContent(name); ok && strings.TrimSpace(c) == content {
|
||||
return name, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
13
internal/assets/presets/minimal.json
Normal file
13
internal/assets/presets/minimal.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "minimal",
|
||||
"description": "Ruhiger Login: nur Header-Zeile und dezenter Footer",
|
||||
"scripts": [
|
||||
{ "name": "00-header", "enabled": true },
|
||||
{ "name": "10-help-text", "enabled": false },
|
||||
{ "name": "50-landscape-sysinfo", "enabled": false },
|
||||
{ "name": "50-motd-news", "enabled": false },
|
||||
{ "name": "80-livepatch", "enabled": false },
|
||||
{ "name": "91-release-upgrade", "enabled": false },
|
||||
{ "name": "95-footer", "enabled": true, "install": "footer" }
|
||||
]
|
||||
}
|
||||
13
internal/assets/presets/server-dashboard.json
Normal file
13
internal/assets/presets/server-dashboard.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "server-dashboard",
|
||||
"description": "Server-Übersicht: Branding-Header, Sysinfo-Balken und Update-Hinweis",
|
||||
"scripts": [
|
||||
{ "name": "00-header", "enabled": false },
|
||||
{ "name": "10-help-text", "enabled": false },
|
||||
{ "name": "50-motd-news", "enabled": false },
|
||||
{ "name": "10-brand-header", "enabled": true, "install": "brand-header" },
|
||||
{ "name": "20-sysinfo-panel", "enabled": true, "install": "sysinfo-panel" },
|
||||
{ "name": "90-updates-available", "enabled": true },
|
||||
{ "name": "95-footer", "enabled": true, "install": "footer" }
|
||||
]
|
||||
}
|
||||
16
internal/assets/presets/ubuntu-default.json
Normal file
16
internal/assets/presets/ubuntu-default.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "ubuntu-default",
|
||||
"description": "Ubuntu-Standard: alle bekannten System-Scripte aktiviert",
|
||||
"scripts": [
|
||||
{ "name": "00-header", "enabled": true },
|
||||
{ "name": "10-help-text", "enabled": true },
|
||||
{ "name": "50-landscape-sysinfo", "enabled": true },
|
||||
{ "name": "50-motd-news", "enabled": true },
|
||||
{ "name": "80-livepatch", "enabled": true },
|
||||
{ "name": "90-updates-available", "enabled": true },
|
||||
{ "name": "91-release-upgrade", "enabled": true },
|
||||
{ "name": "95-hwe-eol", "enabled": true },
|
||||
{ "name": "97-overlayroot", "enabled": true },
|
||||
{ "name": "98-fsck-at-reboot", "enabled": true }
|
||||
]
|
||||
}
|
||||
36
internal/assets/scripts/brand-header
Normal file
36
internal/assets/scripts/brand-header
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#!/bin/sh
|
||||
# motd-assist :: brand-header — Rahmen-Header mit Hostname und Betriebssystem
|
||||
set -u
|
||||
|
||||
E=$(printf '\033')
|
||||
BLUE="${E}[1;38;5;75m"
|
||||
ORANGE="${E}[1;38;5;214m"
|
||||
DIM="${E}[2m"
|
||||
BOLD="${E}[1m"
|
||||
R="${E}[0m"
|
||||
|
||||
HOST=$(hostname 2>/dev/null || echo localhost)
|
||||
HOST=${HOST%%.*}
|
||||
|
||||
OS=Linux
|
||||
if [ -r /etc/os-release ]; then
|
||||
. /etc/os-release
|
||||
OS=${PRETTY_NAME:-Linux}
|
||||
fi
|
||||
|
||||
W=${COLUMNS:-72}
|
||||
case $W in ''|*[!0-9]*) W=72 ;; esac
|
||||
[ "$W" -gt 78 ] && W=78
|
||||
[ "$W" -lt 44 ] && W=44
|
||||
|
||||
INNER=$((W - 2))
|
||||
PLAIN=" ${HOST} - ${OS} "
|
||||
PAD=$((INNER - ${#PLAIN}))
|
||||
[ "$PAD" -lt 0 ] && PAD=0
|
||||
|
||||
RULE=$(awk -v n="$INNER" 'BEGIN{s="";for(i=0;i<n;i++)s=s "─";printf "%s",s}')
|
||||
SPACES=$(printf '%*s' "$PAD" '')
|
||||
|
||||
printf '%b\n' "$BLUE╭$R$DIM$RULE$R$BLUE╮$R"
|
||||
printf '%b\n' "$BLUE│$R $BOLD$BLUE$HOST$R $DIM·$R $ORANGE$OS$R$SPACES $BLUE│$R"
|
||||
printf '%b\n' "$BLUE╰$R$DIM$RULE$R$BLUE╯$R"
|
||||
14
internal/assets/scripts/footer
Normal file
14
internal/assets/scripts/footer
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#!/bin/sh
|
||||
# motd-assist :: footer — dezente Kernel-/Uptime-Zeile
|
||||
set -u
|
||||
|
||||
E=$(printf '\033')
|
||||
DIM="${E}[2m"
|
||||
R="${E}[0m"
|
||||
|
||||
K=$(uname -r 2>/dev/null || echo unknown)
|
||||
UP=$(uptime -p 2>/dev/null)
|
||||
[ -n "$UP" ] || UP=$(awk '{d=int($1/86400); h=int(($1%86400)/3600); printf "up %d days %d hours", d, h}' /proc/uptime 2>/dev/null)
|
||||
|
||||
[ -n "$UP" ] && printf '%b\n' "${DIM}${K} · ${UP}${R}"
|
||||
exit 0
|
||||
66
internal/assets/scripts/sysinfo-panel
Normal file
66
internal/assets/scripts/sysinfo-panel
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
#!/bin/sh
|
||||
# motd-assist :: sysinfo-panel — Load, Speicher, Disk und Updates als farbige Balken
|
||||
set -u
|
||||
|
||||
E=$(printf '\033')
|
||||
GREEN="${E}[38;5;114m"
|
||||
YELLOW="${E}[38;5;221m"
|
||||
RED="${E}[38;5;203m"
|
||||
LBLUE="${E}[38;5;75m"
|
||||
DIM="${E}[2m"
|
||||
R="${E}[0m"
|
||||
|
||||
BAR=22
|
||||
|
||||
# bar <used> <total> — farbiger Balken mit Prozentanzeige
|
||||
bar() {
|
||||
awk -v u="$1" -v t="$2" -v b="$BAR" -v g="$GREEN" -v y="$YELLOW" -v r="$RED" -v d="$DIM" -v rset="$R" 'BEGIN{
|
||||
if (t <= 0) { printf "%sn/a%s", d, rset; exit }
|
||||
pct = u * 100 / t
|
||||
if (pct > 100) pct = 100
|
||||
f = int(pct / 100 * b + 0.5)
|
||||
c = (pct < 60) ? g : ((pct < 85) ? y : r)
|
||||
s = c
|
||||
for (i = 0; i < f; i++) s = s "━"
|
||||
s = s d
|
||||
for (i = f; i < b; i++) s = s "·"
|
||||
printf "%s%s%s %s%3d%%%s", s, rset, c, c, pct, rset
|
||||
}'
|
||||
}
|
||||
|
||||
mem=$(awk '/^MemTotal:/ {t=$2} /^MemAvailable:/ {a=$2} END {if (t>0) printf "%.0f %.0f", t-a, t}' /proc/meminfo 2>/dev/null)
|
||||
[ -n "$mem" ] && printf '%b\n' "$LBLUE MEM $DIM│$R $(bar $mem)"
|
||||
|
||||
disk=$(df -P / 2>/dev/null | awk 'NR==2 {gsub("%","",$5); print $5}')
|
||||
[ -n "$disk" ] && printf '%b\n' "$LBLUE DISK $DIM│$R $(bar $disk 100)"
|
||||
|
||||
if [ -r /proc/loadavg ]; then
|
||||
load=$(awk '{print $1}' /proc/loadavg)
|
||||
ncpu=$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1)
|
||||
case $ncpu in ''|*[!0-9]*) ncpu=1 ;; esac
|
||||
awk -v l="$load" -v n="$ncpu" -v b="$BAR" -v g="$GREEN" -v y="$YELLOW" -v r="$RED" -v d="$DIM" -v rset="$R" -v lbl="$LBLUE" 'BEGIN{
|
||||
pct = l * 100 / n
|
||||
if (pct > 100) pct = 100
|
||||
f = int(pct / 100 * b + 0.5)
|
||||
c = (pct < 60) ? g : ((pct < 85) ? y : r)
|
||||
s = c
|
||||
for (i = 0; i < f; i++) s = s "━"
|
||||
s = s d
|
||||
for (i = f; i < b; i++) s = s "·"
|
||||
printf "%s LOAD %s│%s %s%s%s %s%s/%s%s\n", lbl, d, rset, s, rset, c, c, l, n, rset
|
||||
}'
|
||||
fi
|
||||
|
||||
if [ -r /var/lib/update-notifier/updates-available ]; then
|
||||
upd=$(awk 'NR==1 {print $1; exit}' /var/lib/update-notifier/updates-available 2>/dev/null)
|
||||
case $upd in
|
||||
''|*[!0-9]*) ;;
|
||||
*)
|
||||
awk -v n="$upd" -v g="$GREEN" -v y="$YELLOW" -v r="$RED" -v d="$DIM" -v rset="$R" -v lbl="$LBLUE" 'BEGIN{
|
||||
c = (n == 0) ? g : ((n < 20) ? y : r)
|
||||
txt = (n == 0) ? "System ist auf dem aktuellen Stand" : n " Paket-Updates verfügbar"
|
||||
printf "%s UPD %s│%s %s%s%s\n", lbl, d, rset, c, txt, rset
|
||||
}'
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
Loading…
Add table
Add a link
Reference in a new issue