diff options
author | mjarkk <mkopenga@gmail.com> | 2021-07-27 15:00:37 +0200 |
---|---|---|
committer | mjarkk <mkopenga@gmail.com> | 2021-07-30 15:14:46 +0200 |
commit | 79848087bccd5c87af1dbb44a39753aad1346f8b (patch) | |
tree | 07e4b6eb4b7ed5fdcbde8d697a214b647ddd0536 /pkg/gui/style | |
parent | a3b820fb5f20f4a24028ecbf285d54bbaa7b6974 (diff) |
Switch to github.com/gookit/color for terminal colors
Diffstat (limited to 'pkg/gui/style')
-rw-r--r-- | pkg/gui/style/basic.go | 147 | ||||
-rw-r--r-- | pkg/gui/style/rgb.go | 111 | ||||
-rw-r--r-- | pkg/gui/style/style.go | 97 | ||||
-rw-r--r-- | pkg/gui/style/style_test.go | 314 |
4 files changed, 669 insertions, 0 deletions
diff --git a/pkg/gui/style/basic.go b/pkg/gui/style/basic.go new file mode 100644 index 000000000..3e2c7a067 --- /dev/null +++ b/pkg/gui/style/basic.go @@ -0,0 +1,147 @@ +package style + +import ( + "fmt" + + "github.com/gookit/color" +) + +type BasicTextStyle struct { + fg color.Color + bg color.Color + opts []color.Color + + style color.Style +} + +func (b BasicTextStyle) Sprint(a ...interface{}) string { + return b.style.Sprint(a...) +} + +func (b BasicTextStyle) Sprintf(format string, a ...interface{}) string { + return b.style.Sprintf(format, a...) +} + +func (b BasicTextStyle) deriveStyle() BasicTextStyle { + // b.style[:0] makes sure to use the same slice memory + if b.fg == 0 { + // Fg is most of the time defined so we reverse the check + b.style = b.style[:0] + } else { + b.style = append(b.style[:0], b.fg) + } + + if b.bg != 0 { + b.style = append(b.style, b.bg) + } + + b.style = append(b.style, b.opts...) + return b +} + +func (b BasicTextStyle) setOpt(opt color.Color, v bool, deriveIfChanged bool) BasicTextStyle { + if v { + // Add value + for _, listOpt := range b.opts { + if listOpt == opt { + // Option already added + return b + } + } + + b.opts = append(b.opts, opt) + } else { + // Remove value + for idx, listOpt := range b.opts { + if listOpt == opt { + b.opts = append(b.opts[:idx], b.opts[idx+1:]...) + + if deriveIfChanged { + return b.deriveStyle() + } + return b + } + } + } + + if deriveIfChanged { + return b.deriveStyle() + } + return b +} + +func (b BasicTextStyle) SetBold(v bool) TextStyle { + return b.setOpt(color.OpBold, v, true) +} + +func (b BasicTextStyle) SetReverse(v bool) TextStyle { + return b.setOpt(color.OpReverse, v, true) +} + +func (b BasicTextStyle) SetUnderline(v bool) TextStyle { + return b.setOpt(color.OpUnderscore, v, true) +} + +func (b BasicTextStyle) SetRGBColor(red, green, blue uint8, background bool) TextStyle { + return b.convertToRGB().SetRGBColor(red, green, blue, background) +} + +func (b BasicTextStyle) convertToRGB() RGBTextStyle { + res := RGBTextStyle{ + fg: b.fg.RGB(), + fgSet: b.fg != 0, + opts: b.opts, + } + + if b.bg != 0 { + // Need to convert bg to fg otherwise .RGB wont work + // for more info see https://github.com/gookit/color/issues/39 + rgbBg := (b.bg - 10).RGB() + rgbBg[3] = 1 + res.bg = &rgbBg + res.style = *color.NewRGBStyle(res.fg, rgbBg) + } else { + res.style = *color.NewRGBStyle(res.fg) + } + res.style.SetOpts(b.opts) + + return res +} + +func (b BasicTextStyle) SetColor(other TextStyle) TextStyle { + switch typedOther := other.(type) { + case BasicTextStyle: + if typedOther.fg != 0 { + b.fg = typedOther.fg + } + if typedOther.bg != 0 { + b.bg = typedOther.bg + } + for _, opt := range typedOther.opts { + b = b.setOpt(opt, true, false) + } + return b.deriveStyle() + case RGBTextStyle: + bAsRGB := b.convertToRGB() + + for _, opt := range typedOther.opts { + bAsRGB.setOpt(opt, true) + } + + if typedOther.fgSet { + bAsRGB.fg = typedOther.fg + bAsRGB.style.SetFg(typedOther.fg) + } + + if typedOther.bg != nil { + // Making sure to copy the value + bAsRGB.bg = &color.RGBColor{} + *bAsRGB.bg = *typedOther.bg + bAsRGB.style.SetBg(*typedOther.bg) + } + + return bAsRGB + default: + panic(fmt.Sprintf("got %T but expected BasicTextStyle or RGBTextStyle", typedOther)) + } +} diff --git a/pkg/gui/style/rgb.go b/pkg/gui/style/rgb.go new file mode 100644 index 000000000..bfda6a30b --- /dev/null +++ b/pkg/gui/style/rgb.go @@ -0,0 +1,111 @@ +package style + +import ( + "fmt" + + "github.com/gookit/color" +) + +type RGBTextStyle struct { + opts color.Opts + fgSet bool + fg color.RGBColor + bg *color.RGBColor + style color.RGBStyle +} + +func (b RGBTextStyle) Sprint(a ...interface{}) string { + return b.style.Sprint(a...) +} + +func (b RGBTextStyle) Sprintf(format string, a ...interface{}) string { + return b.style.Sprintf(format, a...) +} + +func (b RGBTextStyle) setOpt(opt color.Color, v bool) RGBTextStyle { + if v { + // Add value + for _, listOpt := range b.opts { + if listOpt == opt { + return b + } + } + b.opts = append(b.opts, opt) + } else { + // Remove value + for idx, listOpt := range b.opts { + if listOpt == opt { + b.opts = append(b.opts[:idx], b.opts[idx+1:]...) + return b + } + } + } + return b +} + +func (b RGBTextStyle) SetBold(v bool) TextStyle { + b = b.setOpt(color.OpBold, v) + b.style.SetOpts(b.opts) + return b +} + +func (b RGBTextStyle) SetReverse(v bool) TextStyle { + b = b.setOpt(color.OpReverse, v) + b.style.SetOpts(b.opts) + return b +} + +func (b RGBTextStyle) SetUnderline(v bool) TextStyle { + b = b.setOpt(color.OpUnderscore, v) + b.style.SetOpts(b.opts) + return b +} + +func (b RGBTextStyle) SetColor(style TextStyle) TextStyle { + var rgbStyle RGBTextStyle + + switch typedStyle := style.(type) { + case BasicTextStyle: + rgbStyle = typedStyle.convertToRGB() + case RGBTextStyle: + rgbStyle = typedStyle + default: + panic(fmt.Sprintf("got %T but expected BasicTextStyle or RGBTextStyle", typedStyle)) + } + + for _, opt := range rgbStyle.GetOpts() { + b = b.setOpt(opt, true) + } + + if rgbStyle.fgSet { + b.fg = rgbStyle.fg + b.style.SetFg(rgbStyle.fg) + b.fgSet = true + } + + if rgbStyle.bg != nil { + // Making sure to copy value + b.bg = &color.RGBColor{} + *b.bg = *rgbStyle.bg + b.style.SetBg(*rgbStyle.bg) + } + + return b +} + +func (b RGBTextStyle) SetRGBColor(red, green, blue uint8, background bool) TextStyle { + parsedColor := color.Rgb(red, green, blue, background) + if background { + b.bg = &parsedColor + b.style.SetBg(parsedColor) + } else { + b.fg = parsedColor + b.style.SetFg(parsedColor) + b.fgSet = true + } + return b +} + +func (b RGBTextStyle) GetOpts() color.Opts { + return b.opts +} diff --git a/pkg/gui/style/style.go b/pkg/gui/style/style.go new file mode 100644 index 000000000..50ea7b523 --- /dev/null +++ b/pkg/gui/style/style.go @@ -0,0 +1,97 @@ +package style + +import ( + "github.com/gookit/color" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +type TextStyle interface { + Sprint(a ...interface{}) string + Sprintf(format string, a ...interface{}) string + SetBold(v bool) TextStyle + SetReverse(v bool) TextStyle + SetUnderline(v bool) TextStyle + SetColor(style TextStyle) TextStyle + SetRGBColor(r, g, b uint8, background bool) TextStyle +} + +var ( + FgWhite = New(color.FgWhite, 0) + FgLightWhite = New(color.FgLightWhite, 0) + FgBlack = New(color.FgBlack, 0) + FgBlackLighter = New(color.FgBlack.Light(), 0) + FgCyan = New(color.FgCyan, 0) + FgRed = New(color.FgRed, 0) + FgGreen = New(color.FgGreen, 0) + FgBlue = New(color.FgBlue, 0) + FgYellow = New(color.FgYellow, 0) + FgMagenta = New(color.FgMagenta, 0) + + BgWhite = New(0, color.BgWhite) + BgBlack = New(0, color.BgBlack) + BgRed = New(0, color.BgRed) + BgGreen = New(0, color.BgGreen) + BgYellow = New(0, color.BgYellow) + BgBlue = New(0, color.BgBlue) + BgMagenta = New(0, color.BgMagenta) + BgCyan = New(0, color.BgCyan) + + AttrUnderline = New(0, 0).SetUnderline(true) + AttrBold = New(0, 0).SetUnderline(true) +) + +func New(fg color.Color, bg color.Color, opts ...color.Color) TextStyle { + return BasicTextStyle{ + fg: fg, + bg: bg, + opts: opts, + style: color.Style{}, + }.deriveStyle() +} + +func SetConfigStyles(s TextStyle, keys []string, background bool) TextStyle { + for _, key := range keys { + colorMap := map[string]struct { + forground TextStyle + background TextStyle + }{ + "default": {FgWhite, BgBlack}, + "black": {FgBlack, BgBlack}, + "red": {FgRed, BgRed}, + "green": {FgGreen, BgGreen}, + "yellow": {FgYellow, BgYellow}, + "blue": {FgBlue, BgBlue}, + "magenta": {FgMagenta, BgMagenta}, + "cyan": {FgCyan, BgCyan}, + "white": {FgWhite, BgWhite}, + } + value, present := colorMap[key] + if present { + if background { + s = s.SetColor(value.background) + } else { + s = s.SetColor(value.forground) + } + continue + } + + if key == "bold" { + s = s.SetBold(true) + continue + } else if key == "reverse" { + s = s.SetReverse(true) + continue + } else if key == "underline" { + s = s.SetUnderline(true) + continue + } + + r, g, b, validHexColor := utils.GetHexColorValues(key) + if validHexColor { + s = s.SetRGBColor(r, g, b, background) + continue + } + } + + return s +} diff --git a/pkg/gui/style/style_test.go b/pkg/gui/style/style_test.go new file mode 100644 index 000000000..ecb3efa70 --- /dev/null +++ b/pkg/gui/style/style_test.go @@ -0,0 +1,314 @@ +package style + +import ( + "testing" + + "github.com/gookit/color" + "github.com/stretchr/testify/assert" +) + +func TestNewStyle(t *testing.T) { + type scenario struct { + name string + fg, bg color.Color + expectedStyle color.Style + } + + scenarios := []scenario{ + { + "no color", + 0, 0, + color.Style{}, + }, + { + "only fg color", + color.FgRed, 0, + color.Style{color.FgRed}, + }, + { + "only bg color", + 0, color.BgRed, + color.Style{color.BgRed}, + }, + { + "fg and bg color", + color.FgBlue, color.BgRed, + color.Style{color.FgBlue, color.BgRed}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + style := New(s.fg, s.bg) + basicStyle, ok := style.(BasicTextStyle) + assert.True(t, ok, "New(..) should return a interface of type BasicTextStyle") + assert.Equal(t, s.fg, basicStyle.fg) + assert.Equal(t, s.bg, basicStyle.bg) + assert.Equal(t, []color.Color(nil), basicStyle.opts) + assert.Equal(t, s.expectedStyle, basicStyle.style) + }) + } +} + +func TestBasicSetColor(t *testing.T) { + type scenario struct { + name string + colorToSet BasicTextStyle + expect BasicTextStyle + } + + scenarios := []scenario{ + { + "empty color", + BasicTextStyle{}, + BasicTextStyle{fg: color.FgRed, bg: color.BgBlue, opts: []color.Color{color.OpBold}}}, + { + "set new fg color", + BasicTextStyle{fg: color.FgCyan}, + BasicTextStyle{fg: color.FgCyan, bg: color.BgBlue, opts: []color.Color{color.OpBold}}, + }, + { + "set new bg color", + BasicTextStyle{bg: color.BgGray}, + BasicTextStyle{fg: color.FgRed, bg: color.BgGray, opts: []color.Color{color.OpBold}}, + }, + { + "set new fg and bg color", + BasicTextStyle{fg: color.FgCyan, bg: color.BgGray}, + BasicTextStyle{fg: color.FgCyan, bg: color.BgGray, opts: []color.Color{color.OpBold}}, + }, + { + "add options", + BasicTextStyle{opts: []color.Color{color.OpUnderscore}}, + BasicTextStyle{fg: color.FgRed, bg: color.BgBlue, opts: []color.Color{color.OpBold, color.OpUnderscore}}, + }, + { + "add options that already exists", + BasicTextStyle{opts: []color.Color{color.OpBold}}, + BasicTextStyle{fg: color.FgRed, bg: color.BgBlue, opts: []color.Color{color.OpBold}}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + style, ok := New(color.FgRed, color.BgBlue). + SetBold(true). + SetColor(s.colorToSet).(BasicTextStyle) + assert.True(t, ok, "SetColor should return a interface of type BasicTextStyle if the input was also BasicTextStyle") + + style.style = nil + assert.Equal(t, s.expect, style) + }) + } +} + +func TestRGBSetColor(t *testing.T) { + type scenario struct { + name string + colorToSet TextStyle + expect RGBTextStyle + } + + red := color.FgRed.RGB() + cyan := color.FgCyan.RGB() + blue := color.FgBlue.RGB() + gray := color.FgGray.RGB() + + toBg := func(c color.RGBColor) *color.RGBColor { + c[3] = 1 + return &c + } + + scenarios := []scenario{ + { + "empty RGBTextStyle input", + RGBTextStyle{}, + RGBTextStyle{fgSet: true, fg: red, bg: toBg(blue), opts: []color.Color{color.OpBold}}, + }, + { + "empty BasicTextStyle input", + BasicTextStyle{}, + RGBTextStyle{fgSet: true, fg: red, bg: toBg(blue), opts: []color.Color{color.OpBold}}, + }, + { + "set fg and bg color using BasicTextStyle", + BasicTextStyle{fg: color.FgCyan, bg: color.BgGray}, + RGBTextStyle{fgSet: true, fg: cyan, bg: toBg(gray), opts: []color.Color{color.OpBold}}, + }, + { + "set fg and bg color using RGBTextStyle", + RGBTextStyle{fgSet: true, fg: cyan, bg: toBg(gray)}, + RGBTextStyle{fgSet: true, fg: cyan, bg: toBg(gray), opts: []color.Color{color.OpBold}}, + }, + { + "add options", + RGBTextStyle{opts: []color.Color{color.OpUnderscore}}, + RGBTextStyle{fgSet: true, fg: red, bg: toBg(blue), opts: []color.Color{color.OpBold, color.OpUnderscore}}, + }, + { + "add options using BasicTextStyle", + BasicTextStyle{opts: []color.Color{color.OpUnderscore}}, + RGBTextStyle{fgSet: true, fg: red, bg: toBg(blue), opts: []color.Color{color.OpBold, color.OpUnderscore}}, + }, + { + "add options that already exists", + RGBTextStyle{opts: []color.Color{color.OpBold}}, + RGBTextStyle{fgSet: true, fg: red, bg: toBg(blue), opts: []color.Color{color.OpBold}}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + style, ok := New(color.FgRed, color.BgBlue).SetBold(true).(BasicTextStyle) + assert.True(t, ok, "SetBold should return a interface of type BasicTextStyle") + + rgbStyle, ok := style.convertToRGB().SetColor(s.colorToSet).(RGBTextStyle) + assert.True(t, ok, "SetColor should return a interface of type RGBTextColor") + + rgbStyle.style = color.RGBStyle{} + assert.Equal(t, s.expect, rgbStyle) + }) + } +} + +func TestConvertBasicToRGB(t *testing.T) { + type scenario struct { + name string + test func(*testing.T) + } + + scenarios := []scenario{ + { + "convert to rgb with fg", + func(t *testing.T) { + basicStyle, ok := New(color.FgRed, 0).(BasicTextStyle) + assert.True(t, ok, "New(..) should return a interface of type BasicTextStyle") + + rgbStyle := basicStyle.convertToRGB() + assert.True(t, rgbStyle.fgSet) + assert.Equal(t, color.RGB(197, 30, 20), rgbStyle.fg) + assert.Nil(t, rgbStyle.bg) + }, + }, + { + "convert to rgb with fg and bg", + func(t *testing.T) { + basicStyle, ok := New(color.FgRed, color.BgRed).(BasicTextStyle) + assert.True(t, ok, "New(..) should return a interface of type BasicTextStyle") + + rgbStyle := basicStyle.convertToRGB() + assert.True(t, rgbStyle.fgSet) + assert.Equal(t, color.RGB(197, 30, 20), rgbStyle.fg) + assert.Equal(t, color.RGB(197, 30, 20, true), *rgbStyle.bg) + }, + }, + { + "convert to rgb using SetRGBColor", + func(t *testing.T) { + style := New(color.FgRed, 0) + rgbStyle, ok := style.SetRGBColor(255, 00, 255, true).(RGBTextStyle) + assert.True(t, ok, "SetRGBColor should return a interface of type RGBTextStyle") + + assert.True(t, rgbStyle.fgSet) + assert.Equal(t, color.RGB(197, 30, 20), rgbStyle.fg) + assert.Equal(t, color.RGB(255, 0, 255, true), *rgbStyle.bg) + }, + }, + { + "convert to rgb using SetRGBColor multiple times", + func(t *testing.T) { + style := New(color.FgRed, 0) + rgbStyle, ok := style.SetRGBColor(00, 255, 255, false).SetRGBColor(255, 00, 255, true).(RGBTextStyle) + assert.True(t, ok, "SetRGBColor should return a interface of type RGBTextStyle") + + assert.True(t, rgbStyle.fgSet) + assert.Equal(t, color.RGB(0, 255, 255), rgbStyle.fg) + assert.Equal(t, color.RGB(255, 0, 255, true), *rgbStyle.bg) + }, + }, + } + + for _, s := range scenarios { + t.Run(s.name, s.test) + } +} + +func TestSettingAtributes(t *testing.T) { + type scenario struct { + name string + test func(s TextStyle) TextStyle + expectedOpts []color.Color + } + + scenarios := []scenario{ + { + "no attributes", + func(s TextStyle) TextStyle { + return s + }, + []color.Color{}, + }, + { + "set single attribute", + func(s TextStyle) TextStyle { + return s.SetBold(true) + }, + []color.Color{color.OpBold}, + }, + { + "set multiple attributes", + func(s TextStyle) TextStyle { + return s.SetBold(true).SetUnderline(true) + }, + []color.Color{color.OpBold, color.OpUnderscore}, + }, + { + "unset a attributes", + func(s TextStyle) TextStyle { + return s.SetBold(true).SetBold(false) + }, + []color.Color{}, + }, + { + "unset a attributes with multiple attributes", + func(s TextStyle) TextStyle { + return s.SetBold(true).SetUnderline(true).SetBold(false) + }, + []color.Color{color.OpUnderscore}, + }, + { + "unset all attributes with multiple attributes", + func(s TextStyle) TextStyle { + return s.SetBold(true).SetUnderline(true).SetBold(false).SetUnderline(false) + }, + []color.Color{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + // Test basic style + style := New(color.FgRed, 0) + basicStyle, ok := style.(BasicTextStyle) + assert.True(t, ok, "New(..) should return a interface of type BasicTextStyle") + basicStyle, ok = s.test(basicStyle).(BasicTextStyle) + assert.True(t, ok, "underlaying type should not be changed after test") + assert.Len(t, basicStyle.opts, len(s.expectedOpts)) + for _, opt := range basicStyle.opts { + assert.Contains(t, s.expectedOpts, opt) + } + for _, opt := range s.expectedOpts { + assert.Contains(t, basicStyle.style, opt) + } + + // Test RGB style + rgbStyle := New(color.FgRed, 0).(BasicTextStyle).convertToRGB() + rgbStyle, ok = s.test(rgbStyle).(RGBTextStyle) + assert.True(t, ok, "underlaying type should not be changed after test") + assert.Len(t, rgbStyle.opts, len(s.expectedOpts)) + for _, opt := range rgbStyle.opts { + assert.Contains(t, s.expectedOpts, opt) + } + }) + } +} |