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
120
internal/motd/sanitize.go
Normal file
120
internal/motd/sanitize.go
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
package motd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
type escapeKind int
|
||||
|
||||
const (
|
||||
escNone escapeKind = iota
|
||||
escSGR
|
||||
)
|
||||
|
||||
// parseEscape klassifiziert eine Escape-Sequenz ab runes[0] == ESC und
|
||||
// liefert die konsumierte Rune-Anzahl. Nur SGR (Farben) ist relevant.
|
||||
func parseEscape(r []rune) (string, int, escapeKind) {
|
||||
if len(r) < 2 {
|
||||
return "", len(r), escNone
|
||||
}
|
||||
switch r[1] {
|
||||
case '[': // CSI: Parameter (0x30–0x3F), Zwischenzeichen (0x20–0x2F), Final
|
||||
j := 2
|
||||
for j < len(r) && r[j] >= 0x30 && r[j] <= 0x3f {
|
||||
j++
|
||||
}
|
||||
for j < len(r) && r[j] >= 0x20 && r[j] <= 0x2f {
|
||||
j++
|
||||
}
|
||||
if j >= len(r) {
|
||||
return "", len(r), escNone // unvollständig
|
||||
}
|
||||
if r[j] == 'm' {
|
||||
return string(r[:j+1]), j + 1, escSGR
|
||||
}
|
||||
return "", j + 1, escNone
|
||||
case ']', 'P', 'X', '^', '_': // OSC/DCS/SOS/PM/APC: bis BEL oder ST
|
||||
for j := 2; j < len(r); j++ {
|
||||
if r[j] == 0x07 {
|
||||
return "", j + 1, escNone
|
||||
}
|
||||
if r[j] == '\x1b' && j+1 < len(r) && r[j+1] == '\\' {
|
||||
return "", j + 2, escNone
|
||||
}
|
||||
}
|
||||
return "", len(r), escNone
|
||||
case '(', ')', '*', '+', '#', '%': // Charset-/Zeichensatz-Wechsel
|
||||
if len(r) >= 3 {
|
||||
return "", 3, escNone
|
||||
}
|
||||
return "", len(r), escNone
|
||||
default:
|
||||
return "", 2, escNone
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize bereinigt Script-Output für die Anzeige in der TUI: behalten
|
||||
// werden SGR-Farbsequenzen und Zeilenumbrüche; Cursor-Steuerung, OSC,
|
||||
// Charset-Switches und andere Steuerzeichen werden entfernt. Jede Zeile
|
||||
// wird auf width Spellen begrenzt und bei Farbnutzung mit Reset
|
||||
// abgeschlossen, damit die Pane-Umrandung nicht verfärbt wird.
|
||||
func Sanitize(s string, width int) string {
|
||||
if width <= 0 {
|
||||
width = 80
|
||||
}
|
||||
var b strings.Builder
|
||||
b.Grow(len(s))
|
||||
lineW, hasSGR := 0, false
|
||||
|
||||
runes := []rune(s)
|
||||
for i := 0; i < len(runes); i++ {
|
||||
r := runes[i]
|
||||
switch {
|
||||
case r == '\x1b':
|
||||
seq, n, kind := parseEscape(runes[i:])
|
||||
i += n - 1
|
||||
if kind == escSGR {
|
||||
b.WriteString(seq)
|
||||
hasSGR = true
|
||||
}
|
||||
case r == '\n':
|
||||
if hasSGR && !endsWithReset(&b) {
|
||||
b.WriteString("\x1b[0m")
|
||||
}
|
||||
b.WriteByte('\n')
|
||||
lineW, hasSGR = 0, false
|
||||
case r == '\r', r == '\a', r == '\b', r == '\v', r == '\f':
|
||||
// ignorieren
|
||||
case r == '\t':
|
||||
for k := 0; k < 4 && lineW < width; k++ {
|
||||
b.WriteByte(' ')
|
||||
lineW++
|
||||
}
|
||||
case r < 0x20 || r == 0x7f:
|
||||
// übrige Steuerzeichen entfernen
|
||||
default:
|
||||
if lineW >= width {
|
||||
continue // Zeile abgeschnitten
|
||||
}
|
||||
w := runewidth.RuneWidth(r)
|
||||
if lineW+w > width {
|
||||
lineW = width // breites Zeichen passt nicht mehr → Schnittstelle
|
||||
continue
|
||||
}
|
||||
b.WriteRune(r)
|
||||
lineW += w
|
||||
}
|
||||
}
|
||||
out := b.String()
|
||||
if hasSGR && !strings.HasSuffix(out, "\x1b[0m") {
|
||||
out += "\x1b[0m" // letzte Zeile ohne Newline: offene Farbe schließen
|
||||
}
|
||||
return strings.TrimRight(out, "\n")
|
||||
}
|
||||
|
||||
// endsWithReset prüft, ob der Builder bereits auf Farbe zurückgesetzt ist.
|
||||
func endsWithReset(b *strings.Builder) bool {
|
||||
return strings.HasSuffix(b.String(), "\x1b[0m")
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue