Extended and true colors

Signed-off-by: jolheiser <john.olheiser@gmail.com>
pull/3/head
jolheiser 2020-10-18 16:47:07 -05:00
parent 5e1722bd43
commit a99fb9facd
Signed by: jolheiser
GPG Key ID: B853ADA5DA7BBF7A
13 changed files with 903 additions and 220 deletions

View File

@ -104,4 +104,12 @@ func main() {
color.Time = color.New(color.FgCyan)
color.Stack = color.New(color.FgGreen)
b.Info("Full options emulating Gitea")
// Extended example
ex := color.NewExtended(color.Gold1, color.DarkGreen)
fmt.Println(ex.Format("Gold on Dark Green"))
// True color example
birb := color.NewTrue(color.MustParseHex("#007D96"), &color.RGB{})
fmt.Println(birb.Format("Birb blue on black"))
}

View File

@ -1,87 +0,0 @@
package color
// Attribute defines a single SGR Code
type Attribute int
// Logger attributes
const (
Reset Attribute = iota
Bold
Faint
Italic
Underline
BlinkSlow
BlinkRapid
ReverseVideo
Concealed
CrossedOut
)
// Foreground text colors
const (
FgBlack Attribute = iota + 30
FgRed
FgGreen
FgYellow
FgBlue
FgMagenta
FgCyan
FgWhite
)
// Foreground Hi-Intensity text colors
const (
FgHiBlack Attribute = iota + 90
FgHiRed
FgHiGreen
FgHiYellow
FgHiBlue
FgHiMagenta
FgHiCyan
FgHiWhite
)
// Background text colors
const (
BgBlack Attribute = iota + 40
BgRed
BgGreen
BgYellow
BgBlue
BgMagenta
BgCyan
BgWhite
)
// Background Hi-Intensity text colors
const (
BgHiBlack Attribute = iota + 100
BgHiRed
BgHiGreen
BgHiYellow
BgHiBlue
BgHiMagenta
BgHiCyan
BgHiWhite
)
var attrCache = make(map[string]*Color)
func attr(a Attribute) *Color {
if c, ok := attrCache[string(a)]; ok {
return c
}
c := New(a)
attrCache[string(a)] = c
return c
}
// Format is a quick way to format a string using a single attribute
func (a Attribute) Format(text string) string {
return attr(a).Format(text)
}
// Format is a quick way to format a formatted string using a single attribute
func (a Attribute) Formatf(text string, v ...interface{}) string {
return attr(a).Formatf(text, v...)
}

90
color/basic.go 100644
View File

@ -0,0 +1,90 @@
package color
import (
"fmt"
"strconv"
"strings"
)
// Interface guard
var _ Color = &Basic{}
// Basic defines a custom color object which is defined by SGR parameters.
type Basic struct {
Attrs []BasicAttribute
}
// BasicAttribute defines a single SGR Code
type BasicAttribute int
// Logger attributes
const (
Reset BasicAttribute = iota
Bold
Faint
Italic
Underline
BlinkSlow
BlinkRapid
ReverseVideo
Concealed
CrossedOut
)
// New returns a new Color with attrs Attributes
func New(attrs ...BasicAttribute) *Basic {
return &Basic{Attrs: attrs}
}
// Add adds new Attributes to a Color
func (b *Basic) Add(attrs ...BasicAttribute) {
for _, attr := range attrs {
has := false
for _, att := range b.Attrs {
if attr == att {
has = true
break
}
}
if !has {
b.Attrs = append(b.Attrs, attr)
}
}
}
// String returns a string representation of the sum of a Color's Attributes
func (b *Basic) String() string {
attrs := 0
for _, attr := range b.Attrs {
attrs += int(attr)
}
return fmt.Sprintf("%d", attrs)
}
func (b *Basic) sequence() string {
format := make([]string, len(b.Attrs))
for i, v := range b.Attrs {
format[i] = strconv.Itoa(int(v))
}
return strings.Join(format, ";")
}
func (b *Basic) wrap(s string) string {
return b.format() + s + b.unformat()
}
func (b *Basic) format() string {
return fmt.Sprintf("%s[%sm", escape, b.sequence())
}
func (b *Basic) unformat() string {
return fmt.Sprintf("%s[%dm", escape, Reset)
}
func (b *Basic) Format(text string) string {
if len(b.Attrs) > 0 {
return b.wrap(text)
}
return text
}

View File

@ -0,0 +1,49 @@
package color
// Foreground text colors
const (
FgBlack BasicAttribute = iota + 30
FgRed
FgGreen
FgYellow
FgBlue
FgMagenta
FgCyan
FgWhite
)
// Foreground Hi-Intensity text colors
const (
FgHiBlack BasicAttribute = iota + 90
FgHiRed
FgHiGreen
FgHiYellow
FgHiBlue
FgHiMagenta
FgHiCyan
FgHiWhite
)
// Background text colors
const (
BgBlack BasicAttribute = iota + 40
BgRed
BgGreen
BgYellow
BgBlue
BgMagenta
BgCyan
BgWhite
)
// Background Hi-Intensity text colors
const (
BgHiBlack BasicAttribute = iota + 100
BgHiRed
BgHiGreen
BgHiYellow
BgHiBlue
BgHiMagenta
BgHiCyan
BgHiWhite
)

View File

@ -0,0 +1,38 @@
package color
import (
"testing"
)
var color = New()
func TestColor(t *testing.T) {
tt := []struct {
Name string
SetAttrs []BasicAttribute
AddAttrs []BasicAttribute
Size int
}{
{"Init", nil, nil, 0},
{"Set 2", []BasicAttribute{Bold, FgBlack}, nil, 2},
{"Add 2", nil, []BasicAttribute{Italic, BgWhite}, 4},
{"Set 3", []BasicAttribute{Bold, FgBlack, BgWhite}, nil, 3},
{"Add 3", nil, []BasicAttribute{Italic, FgWhite, BgBlack}, 6},
{"Add Same", nil, []BasicAttribute{Italic, FgWhite, BgBlack}, 6},
{"Add 2 Same", nil, []BasicAttribute{Italic, FgWhite, BgGreen}, 7},
}
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
if tc.SetAttrs != nil {
color.Attrs = tc.SetAttrs
}
if tc.AddAttrs != nil {
color.Add(tc.AddAttrs...)
}
if len(color.Attrs) != tc.Size {
t.Logf("color has `%d` attributes, but should have `%d`", len(color.Attrs), tc.Size)
}
})
}
}

View File

@ -1,112 +1,17 @@
package color
import (
"fmt"
"regexp"
"strconv"
"strings"
)
const escape = "\x1b"
var (
directive = `{[^}]*}`
directiveRe = regexp.MustCompile("[#]" + directive)
)
// DirectivePrefix changes the directive prefix for parsing color wrap directives
func DirectivePrefix(p rune) {
directiveRe = regexp.MustCompile("[" + string(p) + "]" + directive)
}
// Color defines a custom color object which is defined by SGR parameters.
type Color struct {
Attrs []Attribute
}
// New returns a new Color with attrs Attributes
func New(attrs ...Attribute) *Color {
return &Color{Attrs: attrs}
}
// Add adds new Attributes to a Color
func (c *Color) Add(attrs ...Attribute) {
for _, attr := range attrs {
has := false
for _, att := range c.Attrs {
if attr == att {
has = true
break
}
}
if !has {
c.Attrs = append(c.Attrs, attr)
}
}
}
// String returns a string representation of the sum of a Color's Attributes
func (c *Color) String() string {
attrs := 0
for _, attr := range c.Attrs {
attrs += int(attr)
}
return fmt.Sprintf("%d", attrs)
}
func (c *Color) sequence() string {
format := make([]string, len(c.Attrs))
for i, v := range c.Attrs {
format[i] = strconv.Itoa(int(v))
}
return strings.Join(format, ";")
}
func (c *Color) wrap(s string) string {
return c.format() + s + unformat()
}
func (c *Color) format() string {
return fmt.Sprintf("%s[%sm", escape, c.sequence())
}
func unformat() string {
return fmt.Sprintf("%s[%dm", escape, Reset)
}
// Format returns a string wrapped in the color
func (c *Color) Format(text string) string {
if len(c.Attrs) > 0 {
return c.wrap(text)
}
return text
}
// Formatf returns a formatted string wrapped in the color
func (c *Color) Formatf(format string, v ...interface{}) string {
return c.Format(fmt.Sprintf(format, v...))
}
func parse(text string, primary, secondary *Color) string {
for {
loc := directiveRe.FindStringIndex(text)
if loc == nil {
break
}
text = text[:loc[0]] + unformat() + secondary.Format(text[loc[0]+2:loc[1]-1]) + primary.format() + text[loc[1]:]
}
return text
}
// Wrap returns a string wrapped in a primary color, with #{} directives wrapped in a secondary color
func (c *Color) Wrap(text string, secondary *Color) string {
text = parse(text, c, secondary)
return c.Format(text)
type Color interface {
Format(text string) string
}
// ParseLevel parses a string and returns a Beaver Level's Color, defaulting to Info
func ParseLevel(level string) *Color {
func ParseLevel(level string) Color {
switch strings.ToUpper(level) {
case "T", "TRACE":
return Trace

View File

@ -5,47 +5,14 @@ import (
"testing"
)
var color = New()
func TestMain(m *testing.M) {
os.Exit(m.Run())
}
func TestColor(t *testing.T) {
tt := []struct {
Name string
SetAttrs []Attribute
AddAttrs []Attribute
Size int
}{
{"Init", nil, nil, 0},
{"Set 2", []Attribute{Bold, FgBlack}, nil, 2},
{"Add 2", nil, []Attribute{Italic, BgWhite}, 4},
{"Set 3", []Attribute{Bold, FgBlack, BgWhite}, nil, 3},
{"Add 3", nil, []Attribute{Italic, FgWhite, BgBlack}, 6},
{"Add Same", nil, []Attribute{Italic, FgWhite, BgBlack}, 6},
{"Add 2 Same", nil, []Attribute{Italic, FgWhite, BgGreen}, 7},
}
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
if tc.SetAttrs != nil {
color.Attrs = tc.SetAttrs
}
if tc.AddAttrs != nil {
color.Add(tc.AddAttrs...)
}
if len(color.Attrs) != tc.Size {
t.Logf("color has `%d` attributes, but should have `%d`", len(color.Attrs), tc.Size)
}
})
}
}
func TestParseLevel(t *testing.T) {
tt := []struct {
Parse string
Expected *Color
Expected Color
}{
{"T", Trace},
{"Trace", Trace},

48
color/extended.go 100644
View File

@ -0,0 +1,48 @@
package color
import (
"fmt"
)
// Interface guard
var _ Color = &Extended{}
const (
extendedFG = "38;5;"
extendedBG = "48;5;"
)
// Extended defines a custom color object which is defined by SGR parameters.
type Extended struct {
FG ExtendedAttribute
BG ExtendedAttribute
}
// NewExtended returns a new Color with an ExtendedAttribute FG and BG
func NewExtended(fg, bg ExtendedAttribute) *Extended {
return &Extended{
FG: fg,
BG: bg,
}
}
// String returns a string representation of the sum of a Color's Attributes
func (e *Extended) String() string {
return fmt.Sprintf("%d", e.FG+e.BG)
}
func (e *Extended) wrap(s string) string {
return e.format() + s + e.unformat()
}
func (e *Extended) format() string {
return fmt.Sprintf("%s[%s%dm%s[%s%dm", escape, extendedFG, e.FG, escape, extendedBG, e.BG)
}
func (e *Extended) unformat() string {
return fmt.Sprintf("%s[%dm", escape, Reset)
}
func (e *Extended) Format(text string) string {
return e.wrap(text)
}

View File

@ -0,0 +1,519 @@
package color
// ExtendedAttribute defines an extended color
type ExtendedAttribute int
const (
// #000000
Black ExtendedAttribute = iota
// #800000
Maroon
// #008000
Green
// #808000
Olive
// #000080
Navy
// #800080
Purple
// #008080
Teal
// #c0c0c0
Silver
// #808080
Grey
// #ff0000
Red
// #00ff00
Lime
// #ffff00
Yellow
// #0000ff
Blue
// #ff00ff
Fuchsia
// #00ffff
Aqua
// #ffffff
White
// #000000
Grey0
// #00005f
NavyBlue
// #000087
DarkBlue
// #0000af
Blue3
// #0000d7
Blue3_1
// #0000ff
Blue1
// #005f00
DarkGreen
// #005f5f
DeepSkyBlue4
// #005f87
DeepSkyBlue4_1
// #005faf
DeepSkyBlue4_2
// #005fd7
DodgerBlue3
// #005fff
DodgerBlue2
// #008700
Green4
// #00875f
SpringGreen4
// #008787
Turquoise4
// #0087af
DeepSkyBlue3
// #0087d7
DeepSkyBlue3_1
// #0087ff
DodgerBlue1
// #00af00
Green3
// #00af5f
SpringGreen3
// #00af87
DarkCyan
// #00afaf
LightSeaGreen
// #00afd7
DeepSkyBlue2
// #00afff
DeepSkyBlue1
// #00d700
Green3_1
// #00d75f
SpringGreen3_1
// #00d787
SpringGreen2
// #00d7af
Cyan3
// #00d7d7
DarkTurquoise
// #00d7ff
Turquoise2
// #00ff00
Green1
// #00ff5f
SpringGreen2_1
// #00ff87
SpringGreen1
// #00ffaf
MediumSpringGreen
// #00ffd7
Cyan2
// #00ffff
Cyan1
// #5f0000
DarkRed
// #5f005f
DeepPink4
// #5f0087
Purple4
// #5f00af
Purple4_1
// #5f00d7
Purple3
// #5f00ff
BlueViolet
// #5f5f00
Orange4
// #5f5f5f
Grey37
// #5f5f87
MediumPurple4
// #5f5faf
SlateBlue3
// #5f5fd7
SlateBlue3_1
// #5f5fff
RoyalBlue1
// #5f8700
Chartreuse4
// #5f875f
DarkSeaGreen4
// #5f8787
PaleTurquoise4
// #5f87af
SteelBlue
// #5f87d7
SteelBlue3
// #5f87ff
CornflowerBlue
// #5faf00
Chartreuse3
// #5faf5f
DarkSeaGreen4_1
// #5faf87
CadetBlue
// #5fafaf
CadetBlue_1
// #5fafd7
SkyBlue3
// #5fafff
SteelBlue1
// #5fd700
Chartreuse3_1
// #5fd75f
PaleGreen3
// #5fd787
SeaGreen3
// #5fd7af
Aquamarine3
// #5fd7d7
MediumTurquoise
// #5fd7ff
SteelBlue1_1
// #5fff00
Chartreuse2
// #5fff5f
SeaGreen2
// #5fff87
SeaGreen1
// #5fffaf
SeaGreen1_1
// #5fffd7
Aquamarine1
// #5fffff
DarkSlateGray2
// #870000
DarkRed_1
// #87005f
DeepPink4_1
// #870087
DarkMagenta
// #8700af
DarkMagenta_1
// #8700d7
DarkViolet
// #8700ff
Purple_1
// #875f00
Orange4_1
// #875f5f
LightPink4
// #875f87
Plum4
// #875faf
MediumPurple3
// #875fd7
MediumPurple3_1
// #875fff
SlateBlue1
// #878700
Yellow4
// #87875f
Wheat4
// #878787
Grey53
// #8787af
LightSlateGrey
// #8787d7
MediumPurple
// #8787ff
LightSlateBlue
// #87af00
Yellow4_1
// #87af5f
DarkOliveGreen3
// #87af87
DarkSeaGreen
// #87afaf
LightSkyBlue3
// #87afd7
LightSkyBlue3_1
// #87afff
SkyBlue2
// #87d700
Chartreuse2_1
// #87d75f
DarkOliveGreen3_1
// #87d787
PaleGreen3_1
// #87d7af
DarkSeaGreen3
// #87d7d7
DarkSlateGray3
// #87d7ff
SkyBlue1
// #87ff00
Chartreuse1
// #87ff5f
LightGreen
// #87ff87
LightGreen_1
// #87ffaf
PaleGreen1
// #87ffd7
Aquamarine1_1
// #87ffff
DarkSlateGray1
// #af0000
Red3
// #af005f
DeepPink4_2
// #af0087
MediumVioletRed
// #af00af
Magenta3
// #af00d7
DarkViolet_1
// #af00ff
Purple_2
// #af5f00
DarkOrange3
// #af5f5f
IndianRed
// #af5f87
HotPink3
// #af5faf
MediumOrchid3
// #af5fd7
MediumOrchid
// #af5fff
MediumPurple2
// #af8700
DarkGoldenrod
// #af875f
LightSalmon3
// #af8787
RosyBrown
// #af87af
Grey63
// #af87d7
MediumPurple2_1
// #af87ff
MediumPurple1
// #afaf00
Gold3
// #afaf5f
DarkKhaki
// #afaf87
NavajoWhite3
// #afafaf
Grey69
// #afafd7
LightSteelBlue3
// #afafff
LightSteelBlue
// #afd700
Yellow3
// #afd75f
DarkOliveGreen3_2
// #afd787
DarkSeaGreen3_1
// #afd7af
DarkSeaGreen2
// #afd7d7
LightCyan3
// #afd7ff
LightSkyBlue1
// #afff00
GreenYellow
// #afff5f
DarkOliveGreen2
// #afff87
PaleGreen1_1
// #afffaf
DarkSeaGreen2_1
// #afffd7
DarkSeaGreen1
// #afffff
PaleTurquoise1
// #d70000
Red3_1
// #d7005f
DeepPink3
// #d70087
DeepPink3_1
// #d700af
Magenta3_1
// #d700d7
Magenta3_2
// #d700ff
Magenta2
// #d75f00
DarkOrange3_1
// #d75f5f
IndianRed_1
// #d75f87
HotPink3_1
// #d75faf
HotPink2
// #d75fd7
Orchid
// #d75fff
MediumOrchid1
// #d78700
Orange3
// #d7875f
LightSalmon3_1
// #d78787
LightPink3
// #d787af
Pink3
// #d787d7
Plum3
// #d787ff
Violet
// #d7af00
Gold3_1
// #d7af5f
LightGoldenrod3
// #d7af87
Tan
// #d7afaf
MistyRose3
// #d7afd7
Thistle3
// #d7afff
Plum2
// #d7d700
Yellow3_1
// #d7d75f
Khaki3
// #d7d787
LightGoldenrod2
// #d7d7af
LightYellow3
// #d7d7d7
Grey84
// #d7d7ff
LightSteelBlue1
// #d7ff00
Yellow2
// #d7ff5f
DarkOliveGreen1
// #d7ff87
DarkOliveGreen1_1
// #d7ffaf
DarkSeaGreen1_1
// #d7ffd7
Honeydew2
// #d7ffff
LightCyan1
// #ff0000
Red1
// #ff005f
DeepPink2
// #ff0087
DeepPink1
// #ff00af
DeepPink1_1
// #ff00d7
Magenta2_1
// #ff00ff
Magenta1
// #ff5f00
OrangeRed1
// #ff5f5f
IndianRed1
// #ff5f87
IndianRed1_1
// #ff5faf
HotPink
// #ff5fd7
HotPink_1
// #ff5fff
MediumOrchid1_1
// #ff8700
DarkOrange
// #ff875f
Salmon1
// #ff8787
LightCoral
// #ff87af
PaleVioletRed1
// #ff87d7
Orchid2
// #ff87ff
Orchid1
// #ffaf00
Orange1
// #ffaf5f
SandyBrown
// #ffaf87
LightSalmon1
// #ffafaf
LightPink1
// #ffafd7
Pink1
// #ffafff
Plum1
// #ffd700
Gold1
// #ffd75f
LightGoldenrod2_1
// #ffd787
LightGoldenrod2_2
// #ffd7af
NavajoWhite1
// #ffd7d7
MistyRose1
// #ffd7ff
Thistle1
// #ffff00
Yellow1
// #ffff5f
LightGoldenrod1
// #ffff87
Khaki1
// #ffffaf
Wheat1
// #ffffd7
Cornsilk1
// #ffffff
Grey100
// #080808
Grey3
// #121212
Grey7
// #1c1c1c
Grey11
// #262626
Grey15
// #303030
Grey19
// #3a3a3a
Grey23
// #444444
Grey27
// #4e4e4e
Grey30
// #585858
Grey35
// #626262
Grey39
// #6c6c6c
Grey42
// #767676
Grey46
// #808080
Grey50
// #8a8a8a
Grey54
// #949494
Grey58
// #9e9e9e
Grey62
// #a8a8a8
Grey66
// #b2b2b2
Grey70
// #bcbcbc
Grey74
// #c6c6c6
Grey78
// #d0d0d0
Grey82
// #dadada
Grey85
// #e4e4e4
Grey89
// #eeeeee
Grey93
)

66
color/rgb.go 100644
View File

@ -0,0 +1,66 @@
package color
import (
"encoding/hex"
"errors"
"fmt"
"strings"
)
type RGB struct {
Red int
Green int
Blue int
}
func MustParseHex(hexColor string) *RGB {
c, err := ParseHex(hexColor)
if err != nil {
panic(err)
}
return c
}
func ParseHex(hexColor string) (*RGB, error) {
hexColor = strings.TrimPrefix(hexColor, "#")
// Convert to individual parts
var rh, gh, bh string
switch len(hexColor) {
case 3:
rh, gh, bh = hexColor[:1]+hexColor[:1], hexColor[1:2]+hexColor[1:2], hexColor[2:3]+hexColor[2:3]
case 6:
rh, gh, bh = hexColor[0:2], hexColor[2:4], hexColor[4:6]
default:
return nil, errors.New("invalid hex string")
}
// Convert to bytes
rb, err := hex.DecodeString(rh)
if err != nil {
return nil, err
}
gb, err := hex.DecodeString(gh)
if err != nil {
return nil, err
}
bb, err := hex.DecodeString(bh)
if err != nil {
return nil, err
}
return &RGB{
Red: int(rb[0]),
Green: int(gb[0]),
Blue: int(bb[0]),
}, nil
}
// String returns an r;g;b representation of an RGB
func (r *RGB) String() string {
return r.format()
}
func (r *RGB) format() string {
return fmt.Sprintf("%d;%d;%d", r.Red, r.Green, r.Blue)
}

35
color/rgb_test.go 100644
View File

@ -0,0 +1,35 @@
package color
import "testing"
func TestParseHex(t *testing.T) {
tt := []struct {
Name string
Hex string
RGB *RGB
Error bool
}{
{Name: "WhiteShort", Hex: "FFF", RGB: &RGB{255, 255, 255}, Error: false},
{Name: "WhiteLong", Hex: "FFFFFF", RGB: &RGB{255, 255, 255}, Error: false},
{Name: "WhiteInvalid", Hex: "F", RGB: nil, Error: true},
{Name: "InvalidHex", Hex: "ZZZ", RGB: nil, Error: true},
}
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
rgb, err := ParseHex(tc.Hex)
if err != nil {
if tc.Error {
return // Pass
}
t.Log(err)
t.Fail()
}
if rgb.Red != tc.RGB.Red || rgb.Green != tc.RGB.Green || rgb.Blue != tc.RGB.Blue {
t.Logf("hex was parsed incorrectly: parsed %#v vs expected %#v", rgb, tc.RGB)
t.Fail()
}
})
}
}

45
color/true.go 100644
View File

@ -0,0 +1,45 @@
package color
import "fmt"
// Interface guard
var _ Color = &True{}
const (
trueFG = "38;2;"
trueBG = "48;2;"
)
type True struct {
FG *RGB
BG *RGB
}
// NewTrue returns a new Color with RGB FG and BG
func NewTrue(fg, bg *RGB) *True {
return &True{
FG: fg,
BG: bg,
}
}
// String returns a string representation of the sum of a Color's Attributes
func (t *True) String() string {
return fmt.Sprintf("%d", t.FG.Red+t.FG.Green+t.FG.Blue+t.BG.Red+t.BG.Green+t.BG.Blue)
}
func (t *True) wrap(s string) string {
return t.format() + s + t.unformat()
}
func (t *True) format() string {
return fmt.Sprintf("%s[%s%sm%s[%s%sm", escape, trueFG, t.FG.format(), escape, trueBG, t.BG.format())
}
func (t *True) unformat() string {
return fmt.Sprintf("%s[%dm", escape, Reset)
}
func (t *True) Format(text string) string {
return t.wrap(text)
}

View File

@ -63,7 +63,7 @@ func (l Level) Prefix() string {
}
// Color returns a Level's color, defaulting to bold
func (l Level) Color() *color.Color {
func (l Level) Color() color.Color {
switch l {
case TRACE:
return color.Trace