summaryrefslogtreecommitdiffstats
path: root/pkg/gui/style
diff options
context:
space:
mode:
authormjarkk <mkopenga@gmail.com>2021-07-27 15:00:37 +0200
committermjarkk <mkopenga@gmail.com>2021-07-30 15:14:46 +0200
commit79848087bccd5c87af1dbb44a39753aad1346f8b (patch)
tree07e4b6eb4b7ed5fdcbde8d697a214b647ddd0536 /pkg/gui/style
parenta3b820fb5f20f4a24028ecbf285d54bbaa7b6974 (diff)
Switch to github.com/gookit/color for terminal colors
Diffstat (limited to 'pkg/gui/style')
-rw-r--r--pkg/gui/style/basic.go147
-rw-r--r--pkg/gui/style/rgb.go111
-rw-r--r--pkg/gui/style/style.go97
-rw-r--r--pkg/gui/style/style_test.go314
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)
+ }
+ })
+ }
+}