diff --git a/pkg/gui/style/basic.go b/pkg/gui/style/basic.go index 5cb1ec8aa..45652097e 100644 --- a/pkg/gui/style/basic.go +++ b/pkg/gui/style/basic.go @@ -4,10 +4,33 @@ import ( "github.com/gookit/color" ) +// A TextStyle contains a foreground color, background color, and +// decorations (bold/underline/reverse). +// +// Colors may each be either 16-bit or 256-bit RGB colors. When +// we need to produce a string with a TextStyle, if either foreground or +// background color is RGB, we'll promote the other color component to RGB as well. +// We could simplify this code by forcing everything to be RGB, but we're not +// sure how compatible or efficient that would be with various terminals. +// Lazygit will typically stick to 16-bit colors, but users may configure RGB colors. +// +// TextStyles are value objects, not entities, so for example if you want to +// add the bold decoration to a TextStyle, we'll create a new TextStyle with +// that decoration applied. +// +// Decorations are additive, so when we merge two TextStyles, if either is bold +// then the resulting style will also be bold. +// +// So that we aren't rederiving the underlying style each time we want to print +// a string, we derive it when a new TextStyle is created and store it in the +// `style` field. + type TextStyle struct { fg *Color bg *Color decoration Decoration + + style Sprinter } type Sprinter interface { @@ -15,74 +38,66 @@ type Sprinter interface { Sprintf(format string, a ...interface{}) string } +func New() TextStyle { + s := TextStyle{} + s.style = s.deriveStyle() + return s +} + +func FromBasicFg(fg color.Color) TextStyle { + s := New() + c := NewBasicColor(fg) + s.fg = &c + s.style = s.deriveStyle() + return s +} + +func FromBasicBg(bg color.Color) TextStyle { + s := New() + c := NewBasicColor(bg) + s.bg = &c + s.style = s.deriveStyle() + return s +} + func (b TextStyle) Sprint(a ...interface{}) string { - return b.deriveStyle().Sprint(a...) + return b.style.Sprint(a...) } func (b TextStyle) Sprintf(format string, a ...interface{}) string { - return b.deriveStyle().Sprintf(format, a...) + return b.style.Sprintf(format, a...) } func (b TextStyle) SetBold() TextStyle { b.decoration.SetBold() + b.style = b.deriveStyle() return b } func (b TextStyle) SetUnderline() TextStyle { b.decoration.SetUnderline() + b.style = b.deriveStyle() return b } func (b TextStyle) SetReverse() TextStyle { b.decoration.SetReverse() + b.style = b.deriveStyle() return b } -func (b TextStyle) deriveStyle() Sprinter { - // TODO: consider caching - return deriveStyle(b.fg, b.bg, b.decoration) +func (b TextStyle) SetBg(color Color) TextStyle { + b.bg = &color + b.style = b.deriveStyle() + return b } -func deriveStyle(fg *Color, bg *Color, decoration Decoration) Sprinter { - if fg == nil && bg == nil { - return color.Style(decoration.ToOpts()) - } - - isRgb := (fg != nil && fg.IsRGB()) || (bg != nil && bg.IsRGB()) - if isRgb { - s := &color.RGBStyle{} - if fg != nil { - s.SetFg(*fg.ToRGB().rgb) - } - if bg != nil { - s.SetBg(*bg.ToRGB().rgb) - } - s.SetOpts(decoration.ToOpts()) - return s - } - - style := make([]color.Color, 0, 5) - - if fg != nil { - style = append(style, *fg.basic) - } - - if bg != nil { - style = append(style, *bg.basic) - } - - style = append(style, decoration.ToOpts()...) - - return color.Style(style) +func (b TextStyle) SetFg(color Color) TextStyle { + b.fg = &color + b.style = b.deriveStyle() + return b } -// // 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) - func (b TextStyle) MergeStyle(other TextStyle) TextStyle { b.decoration = b.decoration.Merge(other.decoration) @@ -94,5 +109,52 @@ func (b TextStyle) MergeStyle(other TextStyle) TextStyle { b.bg = other.bg } + b.style = b.deriveStyle() + return b } + +func (b TextStyle) deriveStyle() Sprinter { + if b.fg == nil && b.bg == nil { + return color.Style(b.decoration.ToOpts()) + } + + isRgb := (b.fg != nil && b.fg.IsRGB()) || (b.bg != nil && b.bg.IsRGB()) + if isRgb { + return b.deriveRGBStyle() + } + + return b.deriveBasicStyle() +} + +func (b TextStyle) deriveBasicStyle() color.Style { + style := make([]color.Color, 0, 5) + + if b.fg != nil { + style = append(style, *b.fg.basic) + } + + if b.bg != nil { + style = append(style, *b.bg.basic) + } + + style = append(style, b.decoration.ToOpts()...) + + return color.Style(style) +} + +func (b TextStyle) deriveRGBStyle() *color.RGBStyle { + style := &color.RGBStyle{} + + if b.fg != nil { + style.SetFg(*b.fg.ToRGB().rgb) + } + + if b.bg != nil { + style.SetBg(*b.bg.ToRGB().rgb) + } + + style.SetOpts(b.decoration.ToOpts()) + + return style +} diff --git a/pkg/gui/style/color.go b/pkg/gui/style/color.go index ddd8549aa..b2df21bbf 100644 --- a/pkg/gui/style/color.go +++ b/pkg/gui/style/color.go @@ -19,17 +19,14 @@ func NewBasicColor(cl color.Color) Color { return c } -func (c *Color) IsRGB() bool { +func (c Color) IsRGB() bool { return c.rgb != nil } -func (c *Color) ToRGB() Color { +func (c Color) ToRGB() Color { if c.IsRGB() { - return *c + return c } - rgb := c.basic.RGB() - c.rgb = &rgb - - return NewRGBColor(rgb) + return NewRGBColor(c.basic.RGB()) } diff --git a/pkg/gui/style/decoration.go b/pkg/gui/style/decoration.go index 165a54084..51887fbdd 100644 --- a/pkg/gui/style/decoration.go +++ b/pkg/gui/style/decoration.go @@ -42,9 +42,11 @@ func (d Decoration) Merge(other Decoration) Decoration { if other.bold { d.bold = true } + if other.underline { d.underline = true } + if other.reverse { d.reverse = true } diff --git a/pkg/gui/style/style.go b/pkg/gui/style/style.go index 5911546c7..d6c8149b2 100644 --- a/pkg/gui/style/style.go +++ b/pkg/gui/style/style.go @@ -2,11 +2,9 @@ package style import ( "github.com/gookit/color" - "github.com/jesseduffield/lazygit/pkg/utils" ) var ( - // FgWhite = New(pointerTo(color.FgWhite), nil) FgWhite = FromBasicFg(color.FgWhite) FgLightWhite = FromBasicFg(color.FgLightWhite) FgBlack = FromBasicFg(color.FgBlack) @@ -30,67 +28,3 @@ var ( AttrUnderline = New().SetUnderline() AttrBold = New().SetBold() ) - -func New() TextStyle { - return TextStyle{} -} - -func FromBasicFg(fg color.Color) TextStyle { - s := New() - c := NewBasicColor(fg) - s.fg = &c - return s -} - -func FromBasicBg(bg color.Color) TextStyle { - s := New() - c := NewBasicColor(bg) - s.bg = &c - return s -} - -var 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}, -} - -func SetConfigStyles(keys []string, background bool) TextStyle { - s := New() - - for _, key := range keys { - switch key { - case "bold": - s = s.SetBold() - case "reverse": - s = s.SetReverse() - case "underline": - s = s.SetUnderline() - default: - value, present := colorMap[key] - if present { - var c TextStyle - if background { - c = value.background - } else { - c = value.forground - } - s = s.MergeStyle(c) - } else if utils.IsValidHexValue(key) { - c := NewRGBColor(color.HEX(key, background)) - s.bg = &c - } - } - } - - return s -} diff --git a/pkg/theme/theme.go b/pkg/theme/theme.go index ffccc606a..d05897540 100644 --- a/pkg/theme/theme.go +++ b/pkg/theme/theme.go @@ -42,13 +42,13 @@ var ( // UpdateTheme updates all theme variables func UpdateTheme(themeConfig config.ThemeConfig) { - ActiveBorderColor = GetGocuiColor(themeConfig.ActiveBorderColor) - InactiveBorderColor = GetGocuiColor(themeConfig.InactiveBorderColor) - SelectedLineBgColor = style.SetConfigStyles(themeConfig.SelectedLineBgColor, true) - SelectedRangeBgColor = style.SetConfigStyles(themeConfig.SelectedRangeBgColor, true) - GocuiSelectedLineBgColor = GetGocuiColor(themeConfig.SelectedLineBgColor) - OptionsColor = GetGocuiColor(themeConfig.OptionsTextColor) - OptionsFgColor = style.SetConfigStyles(themeConfig.OptionsTextColor, false) + ActiveBorderColor = GetGocuiStyle(themeConfig.ActiveBorderColor) + InactiveBorderColor = GetGocuiStyle(themeConfig.InactiveBorderColor) + SelectedLineBgColor = GetTextStyle(themeConfig.SelectedLineBgColor, true) + SelectedRangeBgColor = GetTextStyle(themeConfig.SelectedRangeBgColor, true) + GocuiSelectedLineBgColor = GetGocuiStyle(themeConfig.SelectedLineBgColor) + OptionsColor = GetGocuiStyle(themeConfig.OptionsTextColor) + OptionsFgColor = GetTextStyle(themeConfig.OptionsTextColor, false) isLightTheme := themeConfig.LightTheme if isLightTheme { @@ -90,11 +90,61 @@ func GetGocuiAttribute(key string) gocui.Attribute { return gocui.ColorWhite } -// GetGocuiColor bitwise OR's a list of attributes obtained via the given keys -func GetGocuiColor(keys []string) gocui.Attribute { +// GetGocuiStyle bitwise OR's a list of attributes obtained via the given keys +func GetGocuiStyle(keys []string) gocui.Attribute { var attribute gocui.Attribute for _, key := range keys { attribute |= GetGocuiAttribute(key) } return attribute } + +var colorMap = map[string]struct { + foreground style.TextStyle + background style.TextStyle +}{ + "default": {style.FgWhite, style.BgBlack}, + "black": {style.FgBlack, style.BgBlack}, + "red": {style.FgRed, style.BgRed}, + "green": {style.FgGreen, style.BgGreen}, + "yellow": {style.FgYellow, style.BgYellow}, + "blue": {style.FgBlue, style.BgBlue}, + "magenta": {style.FgMagenta, style.BgMagenta}, + "cyan": {style.FgCyan, style.BgCyan}, + "white": {style.FgWhite, style.BgWhite}, +} + +func GetTextStyle(keys []string, background bool) style.TextStyle { + s := style.New() + + for _, key := range keys { + switch key { + case "bold": + s = s.SetBold() + case "reverse": + s = s.SetReverse() + case "underline": + s = s.SetUnderline() + default: + value, present := colorMap[key] + if present { + var c style.TextStyle + if background { + c = value.background + } else { + c = value.foreground + } + s = s.MergeStyle(c) + } else if utils.IsValidHexValue(key) { + c := style.NewRGBColor(color.HEX(key, background)) + if background { + s.SetBg(c) + } else { + s.SetFg(c) + } + } + } + } + + return s +}