diff --git a/pkg/commands/patch/patch_parser.go b/pkg/commands/patch/patch_parser.go index 66e100ece..12426a3df 100644 --- a/pkg/commands/patch/patch_parser.go +++ b/pkg/commands/patch/patch_parser.go @@ -98,35 +98,35 @@ func (l *PatchLine) render(selected bool, included bool) string { return coloredString(style.FgCyan, match[1], selected, included) + coloredString(theme.DefaultTextColor, match[2], selected, false) } - colorAttr := theme.DefaultTextColor + textStyle := theme.DefaultTextColor switch l.Kind { case PATCH_HEADER: - colorAttr = colorAttr.SetBold(true) + textStyle = textStyle.SetBold() case ADDITION: - colorAttr = colorAttr.SetColor(style.FgGreen) + textStyle = style.FgGreen case DELETION: - colorAttr = colorAttr.SetColor(style.FgRed) + textStyle = style.FgRed case COMMIT_SHA: - colorAttr = colorAttr.SetColor(style.FgYellow) + textStyle = style.FgYellow } - return coloredString(colorAttr, content, selected, included) + return coloredString(textStyle, content, selected, included) } -func coloredString(colorAttr style.TextStyle, str string, selected bool, included bool) string { +func coloredString(textStyle style.TextStyle, str string, selected bool, included bool) string { if selected { - colorAttr = colorAttr.SetColor(theme.SelectedRangeBgColor) + textStyle = textStyle.MergeStyle(theme.SelectedRangeBgColor) } if len(str) < 2 { - return colorAttr.Sprint(str) + return textStyle.Sprint(str) } - res := colorAttr.Sprint(str[:1]) + res := textStyle.Sprint(str[:1]) if included { - return res + colorAttr.SetColor(style.BgGreen).Sprint(str[1:]) + return res + textStyle.MergeStyle(style.BgGreen).Sprint(str[1:]) } - return res + colorAttr.Sprint(str[1:]) + return res + textStyle.Sprint(str[1:]) } func parsePatch(patch string) ([]int, []int, []*PatchLine) { diff --git a/pkg/gui/command_log_panel.go b/pkg/gui/command_log_panel.go index 6561f64cc..df3410da6 100644 --- a/pkg/gui/command_log_panel.go +++ b/pkg/gui/command_log_panel.go @@ -28,13 +28,13 @@ func (gui *Gui) GetOnRunCommand() func(entry oscommands.CmdLogEntry) { currentSpan = entry.GetSpan() } - clrAttr := theme.DefaultTextColor + textStyle := theme.DefaultTextColor if !entry.GetCommandLine() { - clrAttr = clrAttr.SetColor(style.FgMagenta) + textStyle = style.FgMagenta } gui.CmdLog = append(gui.CmdLog, entry.GetCmdStr()) indentedCmdStr := " " + strings.Replace(entry.GetCmdStr(), "\n", "\n ", -1) - fmt.Fprint(gui.Views.Extras, "\n"+clrAttr.Sprint(indentedCmdStr)) + fmt.Fprint(gui.Views.Extras, "\n"+textStyle.Sprint(indentedCmdStr)) } } diff --git a/pkg/gui/information_panel.go b/pkg/gui/information_panel.go index a58d2ddfb..d09f495c5 100644 --- a/pkg/gui/information_panel.go +++ b/pkg/gui/information_panel.go @@ -15,8 +15,8 @@ func (gui *Gui) informationStr() string { } if gui.g.Mouse { - donate := style.FgMagenta.SetUnderline(true).Sprint(gui.Tr.Donate) - askQuestion := style.FgYellow.SetUnderline(true).Sprint(gui.Tr.AskQuestion) + donate := style.FgMagenta.SetUnderline().Sprint(gui.Tr.Donate) + askQuestion := style.FgYellow.SetUnderline().Sprint(gui.Tr.AskQuestion) return fmt.Sprintf("%s %s %s", donate, askQuestion, gui.Config.GetVersion()) } else { return gui.Config.GetVersion() diff --git a/pkg/gui/mergeconflicts/rendering.go b/pkg/gui/mergeconflicts/rendering.go index 84708928f..67aa6c49d 100644 --- a/pkg/gui/mergeconflicts/rendering.go +++ b/pkg/gui/mergeconflicts/rendering.go @@ -15,18 +15,18 @@ func ColoredConflictFile(content string, state *State, hasFocus bool) string { conflict, remainingConflicts := shiftConflict(state.conflicts) var outputBuffer bytes.Buffer for i, line := range utils.SplitLines(content) { - colour := theme.DefaultTextColor + textStyle := theme.DefaultTextColor if i == conflict.start || i == conflict.middle || i == conflict.end { - colour.SetColor(style.FgRed) + textStyle = style.FgRed } if hasFocus && state.conflictIndex < len(state.conflicts) && *state.conflicts[state.conflictIndex] == *conflict && shouldHighlightLine(i, conflict, state.conflictTop) { - colour = theme.SelectedRangeBgColor.SetBold(true) + textStyle = theme.SelectedRangeBgColor.SetBold() } if i == conflict.end && len(remainingConflicts) > 0 { conflict, remainingConflicts = shiftConflict(remainingConflicts) } - outputBuffer.WriteString(colour.Sprint(line) + "\n") + outputBuffer.WriteString(textStyle.Sprint(line) + "\n") } return outputBuffer.String() } diff --git a/pkg/gui/modes.go b/pkg/gui/modes.go index 36843e998..af8977989 100644 --- a/pkg/gui/modes.go +++ b/pkg/gui/modes.go @@ -27,7 +27,7 @@ func (gui *Gui) modeStatuses() []modeStatus { { isActive: gui.GitCommand.PatchManager.Active, description: func() string { - return style.FgYellow.SetBold(true).Sprintf( + return style.FgYellow.SetBold().Sprintf( "%s %s", gui.Tr.LcBuildingPatch, style.AttrUnderline.Sprint(gui.Tr.ResetInParentheses), @@ -38,7 +38,7 @@ func (gui *Gui) modeStatuses() []modeStatus { { isActive: gui.State.Modes.Filtering.Active, description: func() string { - return style.FgRed.SetBold(true).Sprintf( + return style.FgRed.SetBold().Sprintf( "%s '%s' %s", gui.Tr.LcFilteringBy, gui.State.Modes.Filtering.GetPath(), diff --git a/pkg/gui/presentation/commits.go b/pkg/gui/presentation/commits.go index b9b9a622e..5823a7153 100644 --- a/pkg/gui/presentation/commits.go +++ b/pkg/gui/presentation/commits.go @@ -10,6 +10,8 @@ import ( "github.com/kyokomi/emoji/v2" ) +var cherryPickedCommitTextStyle = style.FgCyan.MergeStyle(style.BgBlue) + func GetCommitListDisplayStrings(commits []*models.Commit, fullDescription bool, cherryPickedCommitShaMap map[string]bool, diffName string, parseEmoji bool) [][]string { lines := make([][]string, len(commits)) @@ -49,7 +51,7 @@ func getFullDescriptionDisplayStringsForCommit(c *models.Commit, cherryPickedCom // for some reason, setting the background to blue pads out the other commits // horizontally. For the sake of accessibility I'm considering this a feature, // not a bug - shaColor = style.FgCyan.SetColor(style.BgBlue) + shaColor = cherryPickedCommitTextStyle } tagString := "" @@ -57,7 +59,7 @@ func getFullDescriptionDisplayStringsForCommit(c *models.Commit, cherryPickedCom if c.Action != "" { secondColumnString = actionColorMap(c.Action).Sprint(c.Action) } else if c.ExtraInfo != "" { - tagString = theme.DiffTerminalColor.SetBold(true).Sprint(c.ExtraInfo) + " " + tagString = theme.DiffTerminalColor.SetBold().Sprint(c.ExtraInfo) + " " } truncatedAuthor := utils.TruncateWithEllipsis(c.Author, 17) @@ -96,7 +98,7 @@ func getDisplayStringsForCommit(c *models.Commit, cherryPickedCommitShaMap map[s // for some reason, setting the background to blue pads out the other commits // horizontally. For the sake of accessibility I'm considering this a feature, // not a bug - shaColor = style.FgCyan.SetColor(style.BgBlue) + shaColor = cherryPickedCommitTextStyle } actionString := "" @@ -104,7 +106,7 @@ func getDisplayStringsForCommit(c *models.Commit, cherryPickedCommitShaMap map[s if c.Action != "" { actionString = actionColorMap(c.Action).Sprint(utils.WithPadding(c.Action, 7)) + " " } else if len(c.Tags) > 0 { - tagString = theme.DiffTerminalColor.SetBold(true).Sprint(strings.Join(c.Tags, " ")) + " " + tagString = theme.DiffTerminalColor.SetBold().Sprint(strings.Join(c.Tags, " ")) + " " } name := c.Name diff --git a/pkg/gui/presentation/reflog_commits.go b/pkg/gui/presentation/reflog_commits.go index 8c5d48779..b6b21e90f 100644 --- a/pkg/gui/presentation/reflog_commits.go +++ b/pkg/gui/presentation/reflog_commits.go @@ -29,7 +29,7 @@ func GetReflogCommitListDisplayStrings(commits []*models.Commit, fullDescription func coloredReflogSha(c *models.Commit, cherryPickedCommitShaMap map[string]bool) string { shaColor := style.FgBlue if cherryPickedCommitShaMap[c.Sha] { - shaColor = style.FgCyan.SetColor(style.BgBlue) + shaColor = cherryPickedCommitTextStyle } return shaColor.Sprint(c.ShortSha()) diff --git a/pkg/gui/style/basic.go b/pkg/gui/style/basic.go deleted file mode 100644 index 3e2c7a067..000000000 --- a/pkg/gui/style/basic.go +++ /dev/null @@ -1,147 +0,0 @@ -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/basic_styles.go b/pkg/gui/style/basic_styles.go new file mode 100644 index 000000000..ef29904bc --- /dev/null +++ b/pkg/gui/style/basic_styles.go @@ -0,0 +1,38 @@ +package style + +import ( + "github.com/gookit/color" +) + +var ( + FgWhite = FromBasicFg(color.FgWhite) + FgLightWhite = FromBasicFg(color.FgLightWhite) + FgBlack = FromBasicFg(color.FgBlack) + FgBlackLighter = FromBasicFg(color.FgBlack.Light()) + FgCyan = FromBasicFg(color.FgCyan) + FgRed = FromBasicFg(color.FgRed) + FgGreen = FromBasicFg(color.FgGreen) + FgBlue = FromBasicFg(color.FgBlue) + FgYellow = FromBasicFg(color.FgYellow) + FgMagenta = FromBasicFg(color.FgMagenta) + + BgWhite = FromBasicBg(color.BgWhite) + BgBlack = FromBasicBg(color.BgBlack) + BgRed = FromBasicBg(color.BgRed) + BgGreen = FromBasicBg(color.BgGreen) + BgYellow = FromBasicBg(color.BgYellow) + BgBlue = FromBasicBg(color.BgBlue) + BgMagenta = FromBasicBg(color.BgMagenta) + BgCyan = FromBasicBg(color.BgCyan) + + AttrUnderline = New().SetUnderline() + AttrBold = New().SetBold() +) + +func FromBasicFg(fg color.Color) TextStyle { + return New().SetFg(NewBasicColor(fg)) +} + +func FromBasicBg(bg color.Color) TextStyle { + return New().SetBg(NewBasicColor(bg)) +} diff --git a/pkg/gui/style/color.go b/pkg/gui/style/color.go new file mode 100644 index 000000000..b2df21bbf --- /dev/null +++ b/pkg/gui/style/color.go @@ -0,0 +1,32 @@ +package style + +import "github.com/gookit/color" + +type Color struct { + rgb *color.RGBColor + basic *color.Color +} + +func NewRGBColor(cl color.RGBColor) Color { + c := Color{} + c.rgb = &cl + return c +} + +func NewBasicColor(cl color.Color) Color { + c := Color{} + c.basic = &cl + return c +} + +func (c Color) IsRGB() bool { + return c.rgb != nil +} + +func (c Color) ToRGB() Color { + if c.IsRGB() { + return c + } + + return NewRGBColor(c.basic.RGB()) +} diff --git a/pkg/gui/style/decoration.go b/pkg/gui/style/decoration.go new file mode 100644 index 000000000..51887fbdd --- /dev/null +++ b/pkg/gui/style/decoration.go @@ -0,0 +1,55 @@ +package style + +import "github.com/gookit/color" + +type Decoration struct { + bold bool + underline bool + reverse bool +} + +func (d Decoration) SetBold() { + d.bold = true +} + +func (d Decoration) SetUnderline() { + d.underline = true +} + +func (d Decoration) SetReverse() { + d.reverse = true +} + +func (d Decoration) ToOpts() color.Opts { + opts := make([]color.Color, 0, 3) + + if d.bold { + opts = append(opts, color.OpBold) + } + + if d.underline { + opts = append(opts, color.OpUnderscore) + } + + if d.reverse { + opts = append(opts, color.OpReverse) + } + + return opts +} + +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 + } + + return d +} diff --git a/pkg/gui/style/rgb.go b/pkg/gui/style/rgb.go deleted file mode 100644 index bfda6a30b..000000000 --- a/pkg/gui/style/rgb.go +++ /dev/null @@ -1,111 +0,0 @@ -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 deleted file mode 100644 index 50ea7b523..000000000 --- a/pkg/gui/style/style.go +++ /dev/null @@ -1,97 +0,0 @@ -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 index ecb3efa70..669bd4db5 100644 --- a/pkg/gui/style/style_test.go +++ b/pkg/gui/style/style_test.go @@ -53,39 +53,39 @@ func TestNewStyle(t *testing.T) { func TestBasicSetColor(t *testing.T) { type scenario struct { name string - colorToSet BasicTextStyle - expect BasicTextStyle + colorToSet TextStyle + expect TextStyle } scenarios := []scenario{ { "empty color", - BasicTextStyle{}, - BasicTextStyle{fg: color.FgRed, bg: color.BgBlue, opts: []color.Color{color.OpBold}}}, + TextStyle{}, + TextStyle{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}}, + TextStyle{fg: color.FgCyan}, + TextStyle{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}}, + TextStyle{bg: color.BgGray}, + TextStyle{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}}, + TextStyle{fg: color.FgCyan, bg: color.BgGray}, + TextStyle{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}}, + TextStyle{opts: []color.Color{color.OpUnderscore}}, + TextStyle{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}}, + TextStyle{opts: []color.Color{color.OpBold}}, + TextStyle{fg: color.FgRed, bg: color.BgBlue, opts: []color.Color{color.OpBold}}, }, } @@ -127,12 +127,12 @@ func TestRGBSetColor(t *testing.T) { }, { "empty BasicTextStyle input", - BasicTextStyle{}, + TextStyle{}, 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}, + TextStyle{fg: color.FgCyan, bg: color.BgGray}, RGBTextStyle{fgSet: true, fg: cyan, bg: toBg(gray), opts: []color.Color{color.OpBold}}, }, { @@ -147,7 +147,7 @@ func TestRGBSetColor(t *testing.T) { }, { "add options using BasicTextStyle", - BasicTextStyle{opts: []color.Color{color.OpUnderscore}}, + TextStyle{opts: []color.Color{color.OpUnderscore}}, RGBTextStyle{fgSet: true, fg: red, bg: toBg(blue), opts: []color.Color{color.OpBold, color.OpUnderscore}}, }, { @@ -159,10 +159,10 @@ func TestRGBSetColor(t *testing.T) { for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { - style, ok := New(color.FgRed, color.BgBlue).SetBold(true).(BasicTextStyle) + style, ok := New(color.FgRed, color.BgBlue).SetBold().(BasicTextStyle) assert.True(t, ok, "SetBold should return a interface of type BasicTextStyle") - rgbStyle, ok := style.convertToRGB().SetColor(s.colorToSet).(RGBTextStyle) + rgbStyle, ok := style.convertToRGB().MergeStyle(s.colorToSet).(RGBTextStyle) assert.True(t, ok, "SetColor should return a interface of type RGBTextColor") rgbStyle.style = color.RGBStyle{} diff --git a/pkg/gui/style/text_style.go b/pkg/gui/style/text_style.go new file mode 100644 index 000000000..d2bfccaeb --- /dev/null +++ b/pkg/gui/style/text_style.go @@ -0,0 +1,147 @@ +package style + +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 { + Sprint(a ...interface{}) string + Sprintf(format string, a ...interface{}) string +} + +func New() TextStyle { + s := TextStyle{} + s.style = s.deriveStyle() + return s +} + +func (b TextStyle) Sprint(a ...interface{}) string { + return b.style.Sprint(a...) +} + +func (b TextStyle) Sprintf(format string, a ...interface{}) string { + return b.style.Sprintf(format, a...) +} + +// note that our receiver here is not a pointer which means we're receiving a +// copy of the original TextStyle. This allows us to mutate and return that +// TextStyle receiver without actually modifying the original. +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) SetBg(color Color) TextStyle { + b.bg = &color + b.style = b.deriveStyle() + return b +} + +func (b TextStyle) SetFg(color Color) TextStyle { + b.fg = &color + b.style = b.deriveStyle() + return b +} + +func (b TextStyle) MergeStyle(other TextStyle) TextStyle { + b.decoration = b.decoration.Merge(other.decoration) + + if other.fg != nil { + b.fg = other.fg + } + + if other.bg != nil { + 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/theme/gocui.go b/pkg/theme/gocui.go new file mode 100644 index 000000000..6bda0c721 --- /dev/null +++ b/pkg/theme/gocui.go @@ -0,0 +1,45 @@ +package theme + +import ( + "github.com/gookit/color" + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +var gocuiColorMap = map[string]gocui.Attribute{ + "default": gocui.ColorDefault, + "black": gocui.ColorBlack, + "red": gocui.ColorRed, + "green": gocui.ColorGreen, + "yellow": gocui.ColorYellow, + "blue": gocui.ColorBlue, + "magenta": gocui.ColorMagenta, + "cyan": gocui.ColorCyan, + "white": gocui.ColorWhite, + "bold": gocui.AttrBold, + "reverse": gocui.AttrReverse, + "underline": gocui.AttrUnderline, +} + +// GetAttribute gets the gocui color attribute from the string +func GetGocuiAttribute(key string) gocui.Attribute { + if utils.IsValidHexValue(key) { + values := color.HEX(key).Values() + return gocui.NewRGBColor(int32(values[0]), int32(values[1]), int32(values[2])) + } + + value, present := gocuiColorMap[key] + if present { + return value + } + return gocui.ColorWhite +} + +// 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 +} diff --git a/pkg/theme/style.go b/pkg/theme/style.go new file mode 100644 index 000000000..ba014681a --- /dev/null +++ b/pkg/theme/style.go @@ -0,0 +1,57 @@ +package theme + +import ( + "github.com/gookit/color" + "github.com/jesseduffield/lazygit/pkg/gui/style" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +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 +} diff --git a/pkg/theme/theme.go b/pkg/theme/theme.go index 0eafa84b2..b19af5e54 100644 --- a/pkg/theme/theme.go +++ b/pkg/theme/theme.go @@ -1,11 +1,9 @@ package theme import ( - "github.com/gookit/color" "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/gui/style" - "github.com/jesseduffield/lazygit/pkg/utils" ) var ( @@ -24,31 +22,31 @@ var ( OptionsColor gocui.Attribute // DefaultTextColor is the default text color - DefaultTextColor = style.New(color.FgWhite, 0) + DefaultTextColor = style.FgWhite // DefaultHiTextColor is the default highlighted text color - DefaultHiTextColor = style.New(color.FgLightWhite, 0) + DefaultHiTextColor = style.FgLightWhite // SelectedLineBgColor is the background color for the selected line - SelectedLineBgColor = style.New(0, 0) + SelectedLineBgColor = style.New() // SelectedRangeBgColor is the background color of the selected range of lines - SelectedRangeBgColor = style.New(0, 0) + SelectedRangeBgColor = style.New() - OptionsFgColor = style.New(0, 0) + OptionsFgColor = style.New() - DiffTerminalColor = style.New(color.FgMagenta, 0) + DiffTerminalColor = style.FgMagenta ) // UpdateTheme updates all theme variables func UpdateTheme(themeConfig config.ThemeConfig) { - ActiveBorderColor = GetGocuiColor(themeConfig.ActiveBorderColor) - InactiveBorderColor = GetGocuiColor(themeConfig.InactiveBorderColor) - SelectedLineBgColor = style.SetConfigStyles(SelectedLineBgColor, themeConfig.SelectedLineBgColor, true) - SelectedRangeBgColor = style.SetConfigStyles(SelectedRangeBgColor, themeConfig.SelectedRangeBgColor, true) - GocuiSelectedLineBgColor = GetGocuiColor(themeConfig.SelectedLineBgColor) - OptionsColor = GetGocuiColor(themeConfig.OptionsTextColor) - OptionsFgColor = style.SetConfigStyles(OptionsFgColor, 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 { @@ -61,40 +59,3 @@ func UpdateTheme(themeConfig config.ThemeConfig) { GocuiDefaultTextColor = gocui.ColorWhite } } - -// GetAttribute gets the gocui color attribute from the string -func GetGocuiAttribute(key string) gocui.Attribute { - r, g, b, validHexColor := utils.GetHexColorValues(key) - if validHexColor { - return gocui.NewRGBColor(int32(r), int32(g), int32(b)) - } - - colorMap := map[string]gocui.Attribute{ - "default": gocui.ColorDefault, - "black": gocui.ColorBlack, - "red": gocui.ColorRed, - "green": gocui.ColorGreen, - "yellow": gocui.ColorYellow, - "blue": gocui.ColorBlue, - "magenta": gocui.ColorMagenta, - "cyan": gocui.ColorCyan, - "white": gocui.ColorWhite, - "bold": gocui.AttrBold, - "reverse": gocui.AttrReverse, - "underline": gocui.AttrUnderline, - } - value, present := colorMap[key] - if present { - return value - } - return gocui.ColorWhite -} - -// GetGocuiColor bitwise OR's a list of attributes obtained via the given keys -func GetGocuiColor(keys []string) gocui.Attribute { - var attribute gocui.Attribute - for _, key := range keys { - attribute |= GetGocuiAttribute(key) - } - return attribute -} diff --git a/pkg/utils/color.go b/pkg/utils/color.go index 9638b9fcc..0399377c7 100644 --- a/pkg/utils/color.go +++ b/pkg/utils/color.go @@ -1,7 +1,6 @@ package utils import ( - "encoding/hex" "regexp" ) @@ -33,22 +32,23 @@ func getPadWidths(stringArrays [][]string) []int { return padWidths } -// GetHexColorValues returns the rgb values of a hex color -func GetHexColorValues(v string) (r uint8, g uint8, b uint8, valid bool) { - if len(v) == 4 { - v = string([]byte{v[0], v[1], v[1], v[2], v[2], v[3], v[3]}) - } else if len(v) != 7 { - return +func IsValidHexValue(v string) bool { + if len(v) != 4 && len(v) != 7 { + return false } if v[0] != '#' { - return + return false } - rgb, err := hex.DecodeString(v[1:]) - if err != nil { - return + for _, char := range v[1:] { + switch char { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F': + continue + default: + return false + } } - return rgb[0], rgb[1], rgb[2], true + return true }