diff --git a/README.md b/README.md index 62a127a..ee75f47 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,17 @@ colors: text-line: "#00ffff" text-tag: "#005fff" background-tag: "#0087ff" + price-color-scheme: + light: + positive-start: "#22C55E" + positive-end: "#15803D" + negative-start: "#EF4444" + negative-end: "#B91C1C" + dark: + positive-start: "#C6FF40" + positive-end: "#779929" + negative-start: "#FF7940" + negative-end: "#994926" ``` * Terminals supporting TrueColor will be able to represent the full color space and in other cases colors will be down sampled diff --git a/internal/common/common.go b/internal/common/common.go index 51dc1c4..5205edb 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -36,12 +36,28 @@ type Config struct { // ConfigColorScheme represents user defined color scheme type ConfigColorScheme struct { - Text string `yaml:"text"` - TextLight string `yaml:"text-light"` - TextLabel string `yaml:"text-label"` - TextLine string `yaml:"text-line"` - TextTag string `yaml:"text-tag"` - BackgroundTag string `yaml:"background-tag"` + Text string `yaml:"text"` + TextLight string `yaml:"text-light"` + TextLabel string `yaml:"text-label"` + TextLine string `yaml:"text-line"` + TextTag string `yaml:"text-tag"` + BackgroundTag string `yaml:"background-tag"` + PriceColorScheme ConfigPriceColorSchemeGroup `yaml:"price-color-scheme"` +} + +// ConfigPriceColorSchemeGroup represents user defined price color scheme for light and dark mode +type ConfigPriceColorSchemeGroup struct { + Light ConfigPriceColorScheme `yaml:"light"` + Dark ConfigPriceColorScheme `yaml:"dark"` +} + +// ConfigPriceColorScheme represents user defined price color scheme with start and end colors +// for positive and negative price changes +type ConfigPriceColorScheme struct { + PositiveStart string `yaml:"positive-start"` + PositiveEnd string `yaml:"positive-end"` + NegativeStart string `yaml:"negative-start"` + NegativeEnd string `yaml:"negative-end"` } type ConfigAssetGroup struct { diff --git a/internal/ui/util/style.go b/internal/ui/util/style.go index 43e5f61..d546c1e 100644 --- a/internal/ui/util/style.go +++ b/internal/ui/util/style.go @@ -11,15 +11,40 @@ import ( const ( maxPercentChangeColorGradient = 10 + strongChangeThreshold = 10.0 + mediumChangeThreshold = 5.0 ) //nolint:gochecknoglobals var ( - p = te.ColorProfile() - stylePricePositive = newStyleFromGradient("#C6FF40", "#779929") - stylePriceNegative = newStyleFromGradient("#FF7940", "#994926") + p = te.ColorProfile() + colorRegex = regexp.MustCompile(`^#(?:[0-9a-fA-F]{3}){1,2}$`) ) +//nolint:gochecknoglobals +var defaultColorScheme = c.ConfigColorScheme{ + Text: "#d0d0d0", + TextLight: "#8a8a8a", + TextLabel: "#626262", + TextLine: "#3a3a3a", + BackgroundTag: "#303030", + TextTag: "#8a8a8a", + PriceColorScheme: c.ConfigPriceColorSchemeGroup{ + Light: c.ConfigPriceColorScheme{ + PositiveStart: "#22C55E", + PositiveEnd: "#15803D", + NegativeStart: "#EF4444", + NegativeEnd: "#B91C1C", + }, + Dark: c.ConfigPriceColorScheme{ + PositiveStart: "#C6FF40", + PositiveEnd: "#779929", + NegativeStart: "#FF7940", + NegativeEnd: "#994926", + }, + }, +} + // NewStyle creates a new predefined style function func NewStyle(fg string, bg string, bold bool) func(string) string { s := te.Style{}.Foreground(p.Color(fg)).Background(p.Color(bg)) @@ -30,44 +55,47 @@ func NewStyle(fg string, bg string, bold bool) func(string) string { return s.Styled } -func stylePrice(percent float64, text string) string { //nolint:cyclop +func getStylePriceFn(colorScheme c.ConfigPriceColorScheme) func(float64, string) string { + positiveGradient := newStyleFromGradient(colorScheme.PositiveStart, colorScheme.PositiveEnd) + negativeGradient := newStyleFromGradient(colorScheme.NegativeStart, colorScheme.NegativeEnd) - out := te.String(text) + return func(percent float64, text string) string { //nolint:cyclop + if p == te.TrueColor && percent > 0.0 { + return positiveGradient(percent, text) + } - if percent == 0.0 { - return out.Foreground(p.Color("241")).String() - } - - if p == te.TrueColor && percent > 0.0 { - return stylePricePositive(percent, text) - } + if p == te.TrueColor && percent < 0.0 { + return negativeGradient(percent, text) + } - if p == te.TrueColor && percent < 0.0 { - return stylePriceNegative(percent, text) - } + out := te.String(text) - if percent > 10.0 { - return out.Foreground(p.Color("70")).String() - } + if percent == 0.0 { + return out.Foreground(p.Color("241")).String() + } - if percent > 5 { - return out.Foreground(p.Color("76")).String() - } + if percent > strongChangeThreshold { + return out.Foreground(p.Color("70")).String() + } - if percent > 0.0 { - return out.Foreground(p.Color("82")).String() - } + if percent > mediumChangeThreshold { + return out.Foreground(p.Color("76")).String() + } - if percent < -10.0 { - return out.Foreground(p.Color("124")).String() - } + if percent > 0.0 { + return out.Foreground(p.Color("82")).String() + } - if percent < -5.0 { - return out.Foreground(p.Color("160")).String() - } + if percent < strongChangeThreshold*-1 { + return out.Foreground(p.Color("124")).String() + } - return out.Foreground(p.Color("196")).String() + if percent < mediumChangeThreshold*-1 { + return out.Foreground(p.Color("160")).String() + } + return out.Foreground(p.Color("196")).String() + } } func newStyleFromGradient(startColorHex string, endColorHex string) func(float64, string) string { @@ -75,70 +103,83 @@ func newStyleFromGradient(startColorHex string, endColorHex string) func(float64 c2, _ := colorful.Hex(endColorHex) return func(percent float64, text string) string { - normalizedPercent := getNormalizedPercentWithMax(percent, maxPercentChangeColorGradient) textColor := p.Color(c1.BlendHsv(c2, normalizedPercent).Hex()) return te.String(text).Foreground(textColor).String() - } } // Normalize 0-100 percent with a maximum percent value func getNormalizedPercentWithMax(percent float64, maxPercent float64) float64 { - absolutePercent := math.Abs(percent) if absolutePercent >= maxPercent { return 1.0 } return math.Abs(percent / maxPercent) - } // GetColorScheme generates a color scheme based on user defined colors or defaults func GetColorScheme(colorScheme c.ConfigColorScheme) c.Styles { + priceColorScheme := getPriceColorScheme(colorScheme) return c.Styles{ Text: NewStyle( - getColorOrDefault(colorScheme.Text, "#d0d0d0"), + getColorOrDefault(colorScheme.Text, defaultColorScheme.Text), "", false, ), TextLight: NewStyle( - getColorOrDefault(colorScheme.TextLight, "#8a8a8a"), + getColorOrDefault(colorScheme.TextLight, defaultColorScheme.TextLight), "", false, ), TextBold: NewStyle( - getColorOrDefault(colorScheme.Text, "#d0d0d0"), + getColorOrDefault(colorScheme.Text, defaultColorScheme.Text), "", true, ), TextLabel: NewStyle( - getColorOrDefault(colorScheme.TextLabel, "#626262"), + getColorOrDefault(colorScheme.TextLabel, defaultColorScheme.TextLabel), "", false, ), TextLine: NewStyle( - getColorOrDefault(colorScheme.TextLine, "#3a3a3a"), + getColorOrDefault(colorScheme.TextLine, defaultColorScheme.TextLine), "", false, ), - TextPrice: stylePrice, + TextPrice: getStylePriceFn(priceColorScheme), Tag: NewStyle( - getColorOrDefault(colorScheme.TextTag, "#8a8a8a"), - getColorOrDefault(colorScheme.BackgroundTag, "#303030"), + getColorOrDefault(colorScheme.TextTag, defaultColorScheme.TextTag), + getColorOrDefault(colorScheme.BackgroundTag, defaultColorScheme.BackgroundTag), false, ), } +} +// getPriceColorScheme returns the color scheme group for price changes based +// on user defined colors or defaults for light and dark mode +func getPriceColorScheme(colorScheme c.ConfigColorScheme) c.ConfigPriceColorScheme { + source := colorScheme.PriceColorScheme.Light + defaults := defaultColorScheme.PriceColorScheme.Light + + if te.HasDarkBackground() { + source = colorScheme.PriceColorScheme.Dark + defaults = defaultColorScheme.PriceColorScheme.Dark + } + + return c.ConfigPriceColorScheme{ + PositiveStart: getColorOrDefault(source.PositiveStart, defaults.PositiveStart), + PositiveEnd: getColorOrDefault(source.PositiveEnd, defaults.PositiveEnd), + NegativeStart: getColorOrDefault(source.NegativeStart, defaults.NegativeStart), + NegativeEnd: getColorOrDefault(source.NegativeEnd, defaults.NegativeEnd), + } } func getColorOrDefault(colorConfig string, colorDefault string) string { - re := regexp.MustCompile(`^#(?:[0-9a-fA-F]{3}){1,2}$`) - - if len(re.FindStringIndex(colorConfig)) > 0 { + if len(colorRegex.FindStringIndex(colorConfig)) > 0 { return colorConfig }