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") }