// 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 }