Files
2026-01-24 11:17:24 +01:00

297 lines
6.7 KiB
Go

// go-ansisgr provides an SGR (Select Graphic Rendition, a part of ANSI Escape Sequence) parser.
package ansisgr
import (
"unicode"
)
const (
attrReset = 0
attrBold = 1 << iota
attrDim
attrItalic
attrUnderline
attrBlink
attrReverse
attrInvisible
attrStrikethrough
)
const (
colorValid = 1 << (24 + iota)
colorIs16
colorIs256
colorIsRGB
)
// Style represents a set of attributes, foreground color, and background color.
type Style struct {
attr int
foreground int
background int
}
// Mode represents a color syntax.
type Mode int
const (
// ModeNone is used to when color is not specified.
ModeNone Mode = iota
// Mode16 represents 16 colors.
Mode16
// Mode256 represents 256 colors.
Mode256
// ModeRGB represents RGB colors (a.k.a. True Color).
ModeRGB
)
// Color represents color value specified by SGR.
type Color struct{ v int }
// Mode returns the color's Mode.
func (c Color) Mode() Mode {
switch {
case c.v&colorIs16 == colorIs16:
return Mode16
case c.v&colorIs256 == colorIs256:
return Mode256
case c.v&colorIsRGB == colorIsRGB:
return ModeRGB
}
return ModeNone
}
// RGB returns red, green, and blue color values. It assumes that c.Mode() == ModeRGB.
func (c Color) RGB() (int, int, int) {
return 0xff0000 & c.v >> 16, 0x00ff00 & c.v >> 8, 0x0000ff & c.v
}
// Value returns the color value represented by int.
// For example, '\x1b[31m' is 31, '\x1b[38;5;116m' is 116, and '\x1b[38;2;10;20;30' is 660510 (0x0a141e).
func (c Color) Value() int { return c.v & 0xffffff }
// Foreground returns the foreground color. the second return value indicates whether the color is valid or not.
func (s *Style) Foreground() (Color, bool) {
return Color{v: s.foreground}, s.foreground&colorValid == colorValid
}
// Background returns the background color. the second return value indicates whether the color is valid or not.
func (s *Style) Background() (Color, bool) {
return Color{v: s.background}, s.background&colorValid == colorValid
}
// Bold indicates whether bold is enabled.
func (s *Style) Bold() bool { return s.attr&attrBold == attrBold }
// Dim indicates whether dim (faint) is enabled.
func (s *Style) Dim() bool { return s.attr&attrDim == attrDim }
// Italic indicates whether italic is enabled.
func (s *Style) Italic() bool { return s.attr&attrItalic == attrItalic }
// Underline indicates whether underline is enabled.
func (s *Style) Underline() bool { return s.attr&attrUnderline == attrUnderline }
// Blink indicates whether blink is enabled.
func (s *Style) Blink() bool { return s.attr&attrBlink == attrBlink }
// Reverse indicates whether reverse (hidden) is enabled.
func (s *Style) Reverse() bool { return s.attr&attrReverse == attrReverse }
// Invisible indicates whether invisible is enabled.
func (s *Style) Invisible() bool { return s.attr&attrInvisible == attrInvisible }
// Strikethrough indicates whether strikethrough is enabled.
func (s *Style) Strikethrough() bool { return s.attr&attrStrikethrough == attrStrikethrough }
// Iterator is an iterator over a parsed string.
type Iterator struct {
runes []rune
i int
style Style
}
// NewIterator returns a new Iterator.
func NewIterator(s string) *Iterator {
return &Iterator{i: -1, runes: []rune(s)}
}
// Next returns a next rune and its style. the third return value indicates there is a next value.
func (a *Iterator) Next() (rune, Style, bool) {
for a.next() {
r := a.runes[a.i]
if r != 0x1b {
return r, a.style, true
}
if ok := a.next(); ok && a.runes[a.i] != '[' {
continue
}
args := a.consumeSequence()
LOOP:
for len(args) != 0 {
if args[0] == 38 {
offset, consumed := consumeAs256OrRGB(args)
args = args[offset:]
if consumed.valid {
a.style.foreground = consumed.v
continue
}
} else if args[0] == 48 {
offset, consumed := consumeAs256OrRGB(args)
args = args[offset:]
if consumed.valid {
a.style.background = consumed.v
continue
}
}
for _, arg := range args {
switch arg {
case 38, 48:
if len(args) != 1 {
continue LOOP
}
// Ignore when the arg is the last element.
case 0:
a.style.attr = attrReset
a.style.foreground = 0 | colorIs16
a.style.background = 0 | colorIs16
case 39:
a.style.foreground = 0 | colorIs16
case 49:
a.style.background = 0 | colorIs16
case 1:
a.style.attr |= attrBold
case 2:
a.style.attr |= attrDim
case 3:
a.style.attr |= attrItalic
case 4:
a.style.attr |= attrUnderline
case 5:
a.style.attr |= attrBlink
case 7:
a.style.attr |= attrReverse
case 8:
a.style.attr |= attrInvisible
case 9:
a.style.attr |= attrStrikethrough
case 22:
if a.style.attr&attrBold == attrBold {
a.style.attr ^= attrBold
}
if a.style.attr&attrDim == attrDim {
a.style.attr ^= attrDim
}
case 23:
a.style.attr ^= attrItalic
case 24:
a.style.attr ^= attrUnderline
case 25:
a.style.attr ^= attrBlink
case 27:
a.style.attr ^= attrReverse
case 28:
a.style.attr ^= attrInvisible
case 29:
a.style.attr ^= attrStrikethrough
default:
switch {
case (arg >= 30 && arg <= 37):
a.style.foreground = arg | colorIs16 | colorValid
case arg >= 40 && arg <= 47:
a.style.background = arg | colorIs16 | colorValid
case arg >= 90 && arg <= 97:
a.style.foreground = arg | colorIs16 | colorValid
case arg >= 100 && arg <= 107:
a.style.background = arg | colorIs16 | colorValid
}
}
args = args[1:]
}
}
}
return 0, Style{}, false
}
func (a *Iterator) next() bool {
a.i++
return a.i < len(a.runes)
}
func (a *Iterator) consumeSequence() []int {
var args []int
var val int
for a.next() {
switch a.runes[a.i] {
case 'm':
return append(args, val)
case ';':
args = append(args, val)
val = 0
default:
if !unicode.IsDigit(a.runes[a.i]) {
return nil // Broken sequence, ignore
}
val = val*10 + int(a.runes[a.i]-'0')
}
}
return nil
}
type consumedValue struct {
v int
valid bool
}
func consumeAs256OrRGB(args []int) (offset int, v consumedValue) {
if len(args) < 2 || (args[0] != 38 && args[0] != 48) {
return 0, v
}
switch args[1] {
case 5: // 256
if l := len(args); l < 3 {
return l, v
}
if args[2] > 255 {
return 3, v
}
return 3, consumedValue{
v: args[2] | colorValid | colorIs256,
valid: true,
}
case 2: // RGB
if l := len(args); l < 5 {
return l, v
}
var val int
for i := 0; i < 3; i++ {
if args[i+2] > 255 {
return i + 2 + 1, v
}
val |= args[i+2] << (8 * (2 - i))
}
return 5, consumedValue{
v: val | colorValid | colorIsRGB,
valid: true,
}
}
return 2, v
}