Files
MujicaUI/internal/ansi.go
T

165 lines
3.5 KiB
Go

package internal
import (
"log/slog"
"strconv"
"strings"
)
// AnsiColorKind says how an AnsiColor is given.
type AnsiColorKind uint8
const (
AnsiDefault AnsiColorKind = iota
AnsiIndexed
AnsiRGB
)
// AnsiColor is a terminal color: default, a 256-color index, or true color.
type AnsiColor struct {
Kind AnsiColorKind
Index uint8
R, G, B uint8
}
// AnsiStyle is the SGR state a span is drawn with.
type AnsiStyle struct {
FG, BG AnsiColor
Bold, Dim, Italic, Underline, Inverse bool
Strike bool
}
// AnsiSpan is a run of text in one style.
type AnsiSpan struct {
Text string
Style AnsiStyle
}
// ParseAnsi splits s into styled spans from st; unknown escapes stay literal.
func ParseAnsi(s string, st AnsiStyle) ([]AnsiSpan, AnsiStyle) {
var spans []AnsiSpan
var b strings.Builder
flush := func() {
if b.Len() > 0 {
spans = append(spans, AnsiSpan{b.String(), st})
b.Reset()
}
}
for i := 0; i < len(s); {
if s[i] != 0x1b || i+1 >= len(s) || s[i+1] != '[' {
b.WriteByte(s[i])
i++
continue
}
j := i + 2
for j < len(s) && (s[j] < 0x40 || s[j] > 0x7e) {
j++
}
if j >= len(s) {
b.WriteString(s[i:])
break
}
if s[j] != 'm' {
slog.Debug("mujica: ansi unsupported sequence kept", "seq", strconv.Quote(s[i:j+1]))
b.WriteString(s[i : j+1])
i = j + 1
continue
}
flush()
st = applySGR(st, s[i+2:j])
i = j + 1
}
flush()
return spans, st
}
func applySGR(st AnsiStyle, params string) AnsiStyle {
if params == "" {
return AnsiStyle{}
}
codes := strings.Split(params, ";")
for k := 0; k < len(codes); k++ {
n, err := strconv.Atoi(codes[k])
if err != nil {
slog.Debug("mujica: ansi bad SGR parameter", "params", params)
return st
}
switch {
case n == 0:
st = AnsiStyle{}
case n == 1:
st.Bold = true
case n == 2:
st.Dim = true
case n == 3:
st.Italic = true
case n == 4:
st.Underline = true
case n == 7:
st.Inverse = true
case n == 9:
st.Strike = true
case n == 22:
st.Bold, st.Dim = false, false
case n == 23:
st.Italic = false
case n == 24:
st.Underline = false
case n == 27:
st.Inverse = false
case n == 29:
st.Strike = false
case n >= 30 && n <= 37:
st.FG = AnsiColor{Kind: AnsiIndexed, Index: uint8(n - 30)}
case n >= 90 && n <= 97:
st.FG = AnsiColor{Kind: AnsiIndexed, Index: uint8(n - 90 + 8)}
case n == 39:
st.FG = AnsiColor{}
case n >= 40 && n <= 47:
st.BG = AnsiColor{Kind: AnsiIndexed, Index: uint8(n - 40)}
case n >= 100 && n <= 107:
st.BG = AnsiColor{Kind: AnsiIndexed, Index: uint8(n - 100 + 8)}
case n == 49:
st.BG = AnsiColor{}
case n == 38 || n == 48:
c, used, ok := extendedColor(codes[k+1:])
if !ok {
slog.Debug("mujica: ansi bad extended color", "params", params)
return st
}
if n == 38 {
st.FG = c
} else {
st.BG = c
}
k += used
}
}
return st
}
// extendedColor reads "5;n" or "2;r;g;b" after 38 or 48.
func extendedColor(rest []string) (AnsiColor, int, bool) {
num := func(i int) (uint8, bool) {
if i >= len(rest) {
return 0, false
}
v, err := strconv.Atoi(rest[i])
return uint8(v), err == nil && v >= 0 && v <= 255
}
if len(rest) == 0 {
return AnsiColor{}, 0, false
}
switch rest[0] {
case "5":
v, ok := num(1)
return AnsiColor{Kind: AnsiIndexed, Index: v}, 2, ok
case "2":
r, ok1 := num(1)
g, ok2 := num(2)
b, ok3 := num(3)
return AnsiColor{Kind: AnsiRGB, R: r, G: g, B: b}, 4, ok1 && ok2 && ok3
}
return AnsiColor{}, 0, false
}