feat: cycle through colors

resolves #3327
This commit is contained in:
Jan De Dobbeleer 2023-01-09 20:10:55 +01:00 committed by Jan De Dobbeleer
parent 6ca3d22b80
commit 86459f9a2f
13 changed files with 168 additions and 96 deletions

View file

@ -34,9 +34,9 @@ type style struct {
End string End string
} }
type cachedColor struct { type Colors struct {
Background string Background string `json:"background"`
Foreground string Foreground string `json:"foreground"`
} }
const ( const (
@ -75,9 +75,9 @@ const (
// Writer writes colorized ANSI strings // Writer writes colorized ANSI strings
type Writer struct { type Writer struct {
TerminalBackground string TerminalBackground string
Colors *cachedColor Colors *Colors
ParentColors []*cachedColor ParentColors []*Colors
AnsiColors Colors AnsiColors ColorString
Plain bool Plain bool
builder strings.Builder builder strings.Builder
@ -175,7 +175,7 @@ func (w *Writer) Init(shellName string) {
} }
func (w *Writer) SetColors(background, foreground string) { func (w *Writer) SetColors(background, foreground string) {
w.Colors = &cachedColor{ w.Colors = &Colors{
Background: background, Background: background,
Foreground: foreground, Foreground: foreground,
} }
@ -183,9 +183,9 @@ func (w *Writer) SetColors(background, foreground string) {
func (w *Writer) SetParentColors(background, foreground string) { func (w *Writer) SetParentColors(background, foreground string) {
if w.ParentColors == nil { if w.ParentColors == nil {
w.ParentColors = make([]*cachedColor, 0) w.ParentColors = make([]*Colors, 0)
} }
w.ParentColors = append([]*cachedColor{{ w.ParentColors = append([]*Colors{{
Background: background, Background: background,
Foreground: foreground, Foreground: foreground,
}}, w.ParentColors...) }}, w.ParentColors...)
@ -279,7 +279,7 @@ func (w *Writer) Write(background, foreground, text string) {
w.background, w.foreground = w.asAnsiColors(background, foreground) w.background, w.foreground = w.asAnsiColors(background, foreground)
// default to white foreground // default to white foreground
if w.foreground.IsEmpty() { if w.foreground.IsEmpty() {
w.foreground = w.AnsiColors.AnsiColorFromString("white", false) w.foreground = w.AnsiColors.ToColor("white", false)
} }
// validate if we start with a color override // validate if we start with a color override
match := regex.FindNamedRegexMatch(anchorRegex, text) match := regex.FindNamedRegexMatch(anchorRegex, text)
@ -361,7 +361,7 @@ func (w *Writer) writeEscapedAnsiString(text string) {
} }
func (w *Writer) getAnsiFromColorString(colorString string, isBackground bool) Color { func (w *Writer) getAnsiFromColorString(colorString string, isBackground bool) Color {
return w.AnsiColors.AnsiColorFromString(colorString, isBackground) return w.AnsiColors.ToColor(colorString, isBackground)
} }
func (w *Writer) writeSegmentColors() { func (w *Writer) writeSegmentColors() {

View file

@ -13,187 +13,187 @@ func TestWriteANSIColors(t *testing.T) {
Case string Case string
Expected string Expected string
Input string Input string
Colors *cachedColor Colors *Colors
Parent *cachedColor Parent *Colors
TerminalBackground string TerminalBackground string
}{ }{
{ {
Case: "Bold", Case: "Bold",
Input: "<b>test</b>", Input: "<b>test</b>",
Expected: "\x1b[1m\x1b[30mtest\x1b[22m\x1b[0m", Expected: "\x1b[1m\x1b[30mtest\x1b[22m\x1b[0m",
Colors: &cachedColor{Foreground: "black", Background: ParentBackground}, Colors: &Colors{Foreground: "black", Background: ParentBackground},
}, },
{ {
Case: "Bold with color override", Case: "Bold with color override",
Input: "<b><#ffffff>test</></b>", Input: "<b><#ffffff>test</></b>",
Expected: "\x1b[1m\x1b[30m\x1b[38;2;255;255;255mtest\x1b[0m\x1b[30m\x1b[22m\x1b[0m", Expected: "\x1b[1m\x1b[30m\x1b[38;2;255;255;255mtest\x1b[0m\x1b[30m\x1b[22m\x1b[0m",
Colors: &cachedColor{Foreground: "black", Background: ParentBackground}, Colors: &Colors{Foreground: "black", Background: ParentBackground},
}, },
{ {
Case: "Bold with color override, flavor 2", Case: "Bold with color override, flavor 2",
Input: "<#ffffff><b>test</b></>", Input: "<#ffffff><b>test</b></>",
Expected: "\x1b[38;2;255;255;255m\x1b[1mtest\x1b[22m\x1b[0m", Expected: "\x1b[38;2;255;255;255m\x1b[1mtest\x1b[22m\x1b[0m",
Colors: &cachedColor{Foreground: "black", Background: ParentBackground}, Colors: &Colors{Foreground: "black", Background: ParentBackground},
}, },
{ {
Case: "Double override", Case: "Double override",
Input: "<#ffffff>jan</>@<#ffffff>Jans-MBP</>", Input: "<#ffffff>jan</>@<#ffffff>Jans-MBP</>",
Expected: "\x1b[48;2;255;87;51m\x1b[38;2;255;255;255mjan\x1b[32m@\x1b[38;2;255;255;255mJans-MBP\x1b[0m", Expected: "\x1b[48;2;255;87;51m\x1b[38;2;255;255;255mjan\x1b[32m@\x1b[38;2;255;255;255mJans-MBP\x1b[0m",
Colors: &cachedColor{Foreground: "green", Background: "#FF5733"}, Colors: &Colors{Foreground: "green", Background: "#FF5733"},
}, },
{ {
Case: "No color override", Case: "No color override",
Input: "test", Input: "test",
Expected: "\x1b[47m\x1b[30mtest\x1b[0m", Expected: "\x1b[47m\x1b[30mtest\x1b[0m",
Colors: &cachedColor{Foreground: "black", Background: "white"}, Colors: &Colors{Foreground: "black", Background: "white"},
Parent: &cachedColor{Foreground: "black", Background: "white"}, Parent: &Colors{Foreground: "black", Background: "white"},
}, },
{ {
Case: "Inherit foreground", Case: "Inherit foreground",
Input: "test", Input: "test",
Expected: "\x1b[47m\x1b[33mtest\x1b[0m", Expected: "\x1b[47m\x1b[33mtest\x1b[0m",
Colors: &cachedColor{Foreground: ParentForeground, Background: "white"}, Colors: &Colors{Foreground: ParentForeground, Background: "white"},
Parent: &cachedColor{Foreground: "yellow", Background: "white"}, Parent: &Colors{Foreground: "yellow", Background: "white"},
}, },
{ {
Case: "Inherit background", Case: "Inherit background",
Input: "test", Input: "test",
Expected: "\x1b[41m\x1b[30mtest\x1b[0m", Expected: "\x1b[41m\x1b[30mtest\x1b[0m",
Colors: &cachedColor{Foreground: "black", Background: ParentBackground}, Colors: &Colors{Foreground: "black", Background: ParentBackground},
Parent: &cachedColor{Foreground: "yellow", Background: "red"}, Parent: &Colors{Foreground: "yellow", Background: "red"},
}, },
{ {
Case: "No parent", Case: "No parent",
Input: "test", Input: "test",
Expected: "\x1b[30mtest\x1b[0m", Expected: "\x1b[30mtest\x1b[0m",
Colors: &cachedColor{Foreground: "black", Background: ParentBackground}, Colors: &Colors{Foreground: "black", Background: ParentBackground},
}, },
{ {
Case: "Inherit override foreground", Case: "Inherit override foreground",
Input: "hello <parentForeground>world</>", Input: "hello <parentForeground>world</>",
Expected: "\x1b[47m\x1b[30mhello \x1b[33mworld\x1b[0m", Expected: "\x1b[47m\x1b[30mhello \x1b[33mworld\x1b[0m",
Colors: &cachedColor{Foreground: "black", Background: "white"}, Colors: &Colors{Foreground: "black", Background: "white"},
Parent: &cachedColor{Foreground: "yellow", Background: "red"}, Parent: &Colors{Foreground: "yellow", Background: "red"},
}, },
{ {
Case: "Inherit override background", Case: "Inherit override background",
Input: "hello <black,parentBackground>world</>", Input: "hello <black,parentBackground>world</>",
Expected: "\x1b[47m\x1b[30mhello \x1b[41mworld\x1b[0m", Expected: "\x1b[47m\x1b[30mhello \x1b[41mworld\x1b[0m",
Colors: &cachedColor{Foreground: "black", Background: "white"}, Colors: &Colors{Foreground: "black", Background: "white"},
Parent: &cachedColor{Foreground: "yellow", Background: "red"}, Parent: &Colors{Foreground: "yellow", Background: "red"},
}, },
{ {
Case: "Inherit override background, no foreground specified", Case: "Inherit override background, no foreground specified",
Input: "hello <,parentBackground>world</>", Input: "hello <,parentBackground>world</>",
Expected: "\x1b[47m\x1b[30mhello \x1b[41mworld\x1b[0m", Expected: "\x1b[47m\x1b[30mhello \x1b[41mworld\x1b[0m",
Colors: &cachedColor{Foreground: "black", Background: "white"}, Colors: &Colors{Foreground: "black", Background: "white"},
Parent: &cachedColor{Foreground: "yellow", Background: "red"}, Parent: &Colors{Foreground: "yellow", Background: "red"},
}, },
{ {
Case: "Inherit no parent foreground", Case: "Inherit no parent foreground",
Input: "hello <parentForeground>world</>", Input: "hello <parentForeground>world</>",
Expected: "\x1b[47m\x1b[30mhello \x1b[0m\x1b[37;49m\x1b[7mworld\x1b[0m", Expected: "\x1b[47m\x1b[30mhello \x1b[0m\x1b[37;49m\x1b[7mworld\x1b[0m",
Colors: &cachedColor{Foreground: "black", Background: "white"}, Colors: &Colors{Foreground: "black", Background: "white"},
}, },
{ {
Case: "Inherit no parent background", Case: "Inherit no parent background",
Input: "hello <,parentBackground>world</>", Input: "hello <,parentBackground>world</>",
Expected: "\x1b[47m\x1b[30mhello \x1b[0m\x1b[30mworld\x1b[0m", Expected: "\x1b[47m\x1b[30mhello \x1b[0m\x1b[30mworld\x1b[0m",
Colors: &cachedColor{Foreground: "black", Background: "white"}, Colors: &Colors{Foreground: "black", Background: "white"},
}, },
{ {
Case: "Inherit override both", Case: "Inherit override both",
Input: "hello <parentForeground,parentBackground>world</>", Input: "hello <parentForeground,parentBackground>world</>",
Expected: "\x1b[47m\x1b[30mhello \x1b[41m\x1b[33mworld\x1b[0m", Expected: "\x1b[47m\x1b[30mhello \x1b[41m\x1b[33mworld\x1b[0m",
Colors: &cachedColor{Foreground: "black", Background: "white"}, Colors: &Colors{Foreground: "black", Background: "white"},
Parent: &cachedColor{Foreground: "yellow", Background: "red"}, Parent: &Colors{Foreground: "yellow", Background: "red"},
}, },
{ {
Case: "Inherit override both inverted", Case: "Inherit override both inverted",
Input: "hello <parentBackground,parentForeground>world</>", Input: "hello <parentBackground,parentForeground>world</>",
Expected: "\x1b[47m\x1b[30mhello \x1b[43m\x1b[31mworld\x1b[0m", Expected: "\x1b[47m\x1b[30mhello \x1b[43m\x1b[31mworld\x1b[0m",
Colors: &cachedColor{Foreground: "black", Background: "white"}, Colors: &Colors{Foreground: "black", Background: "white"},
Parent: &cachedColor{Foreground: "yellow", Background: "red"}, Parent: &Colors{Foreground: "yellow", Background: "red"},
}, },
{ {
Case: "Inline override", Case: "Inline override",
Input: "hello, <red>world</>, rabbit", Input: "hello, <red>world</>, rabbit",
Expected: "\x1b[47m\x1b[30mhello, \x1b[31mworld\x1b[30m, rabbit\x1b[0m", Expected: "\x1b[47m\x1b[30mhello, \x1b[31mworld\x1b[30m, rabbit\x1b[0m",
Colors: &cachedColor{Foreground: "black", Background: "white"}, Colors: &Colors{Foreground: "black", Background: "white"},
}, },
{ {
Case: "Transparent background", Case: "Transparent background",
Input: "hello world", Input: "hello world",
Expected: "\x1b[37mhello world\x1b[0m", Expected: "\x1b[37mhello world\x1b[0m",
Colors: &cachedColor{Foreground: "white", Background: Transparent}, Colors: &Colors{Foreground: "white", Background: Transparent},
}, },
{ {
Case: "Transparent foreground override", Case: "Transparent foreground override",
Input: "hello <#ffffff>world</>", Input: "hello <#ffffff>world</>",
Expected: "\x1b[32mhello \x1b[38;2;255;255;255mworld\x1b[0m", Expected: "\x1b[32mhello \x1b[38;2;255;255;255mworld\x1b[0m",
Colors: &cachedColor{Foreground: "green", Background: Transparent}, Colors: &Colors{Foreground: "green", Background: Transparent},
}, },
{ {
Case: "No foreground", Case: "No foreground",
Input: "test", Input: "test",
Expected: "\x1b[48;2;255;87;51m\x1b[37mtest\x1b[0m", Expected: "\x1b[48;2;255;87;51m\x1b[37mtest\x1b[0m",
Colors: &cachedColor{Foreground: "", Background: "#FF5733"}, Colors: &Colors{Foreground: "", Background: "#FF5733"},
}, },
{ {
Case: "Transparent foreground", Case: "Transparent foreground",
Input: "test", Input: "test",
Expected: "\x1b[0m\x1b[38;2;255;87;51;49m\x1b[7mtest\x1b[0m", Expected: "\x1b[0m\x1b[38;2;255;87;51;49m\x1b[7mtest\x1b[0m",
Colors: &cachedColor{Foreground: Transparent, Background: "#FF5733"}, Colors: &Colors{Foreground: Transparent, Background: "#FF5733"},
}, },
{ {
Case: "Transparent foreground, terminal background set", Case: "Transparent foreground, terminal background set",
Input: "test", Input: "test",
Expected: "\x1b[38;2;33;47;60m\x1b[48;2;255;87;51mtest\x1b[0m", Expected: "\x1b[38;2;33;47;60m\x1b[48;2;255;87;51mtest\x1b[0m",
Colors: &cachedColor{Foreground: Transparent, Background: "#FF5733"}, Colors: &Colors{Foreground: Transparent, Background: "#FF5733"},
TerminalBackground: "#212F3C", TerminalBackground: "#212F3C",
}, },
{ {
Case: "Foreground for foreground override", Case: "Foreground for foreground override",
Input: "<foreground>test</>", Input: "<foreground>test</>",
Expected: "\x1b[47m\x1b[30mtest\x1b[0m", Expected: "\x1b[47m\x1b[30mtest\x1b[0m",
Colors: &cachedColor{Foreground: "black", Background: "white"}, Colors: &Colors{Foreground: "black", Background: "white"},
}, },
{ {
Case: "Background for background override", Case: "Background for background override",
Input: "<,background>test</>", Input: "<,background>test</>",
Expected: "\x1b[47m\x1b[30mtest\x1b[0m", Expected: "\x1b[47m\x1b[30mtest\x1b[0m",
Colors: &cachedColor{Foreground: "black", Background: "white"}, Colors: &Colors{Foreground: "black", Background: "white"},
}, },
{ {
Case: "Google", Case: "Google",
Input: "<blue,white>G</><red,white>o</><yellow,white>o</><blue,white>g</><green,white>l</><red,white>e</>", Input: "<blue,white>G</><red,white>o</><yellow,white>o</><blue,white>g</><green,white>l</><red,white>e</>",
Expected: "\x1b[47m\x1b[34mG\x1b[40m\x1b[30m\x1b[47m\x1b[31mo\x1b[40m\x1b[30m\x1b[47m\x1b[33mo\x1b[40m\x1b[30m\x1b[47m\x1b[34mg\x1b[40m\x1b[30m\x1b[47m\x1b[32ml\x1b[40m\x1b[30m\x1b[47m\x1b[31me\x1b[0m", //nolint: lll Expected: "\x1b[47m\x1b[34mG\x1b[40m\x1b[30m\x1b[47m\x1b[31mo\x1b[40m\x1b[30m\x1b[47m\x1b[33mo\x1b[40m\x1b[30m\x1b[47m\x1b[34mg\x1b[40m\x1b[30m\x1b[47m\x1b[32ml\x1b[40m\x1b[30m\x1b[47m\x1b[31me\x1b[0m", //nolint: lll
Colors: &cachedColor{Foreground: "black", Background: "black"}, Colors: &Colors{Foreground: "black", Background: "black"},
}, },
{ {
Case: "Foreground for background override", Case: "Foreground for background override",
Input: "<background>test</>", Input: "<background>test</>",
Expected: "\x1b[47m\x1b[37mtest\x1b[0m", Expected: "\x1b[47m\x1b[37mtest\x1b[0m",
Colors: &cachedColor{Foreground: "black", Background: "white"}, Colors: &Colors{Foreground: "black", Background: "white"},
}, },
{ {
Case: "Foreground for background vice versa override", Case: "Foreground for background vice versa override",
Input: "<background,foreground>test</>", Input: "<background,foreground>test</>",
Expected: "\x1b[40m\x1b[37mtest\x1b[0m", Expected: "\x1b[40m\x1b[37mtest\x1b[0m",
Colors: &cachedColor{Foreground: "black", Background: "white"}, Colors: &Colors{Foreground: "black", Background: "white"},
}, },
{ {
Case: "Background for foreground override", Case: "Background for foreground override",
Input: "<,foreground>test</>", Input: "<,foreground>test</>",
Expected: "\x1b[40m\x1b[30mtest\x1b[0m", Expected: "\x1b[40m\x1b[30mtest\x1b[0m",
Colors: &cachedColor{Foreground: "black", Background: "white"}, Colors: &Colors{Foreground: "black", Background: "white"},
}, },
} }
for _, tc := range cases { for _, tc := range cases {
renderer := &Writer{ renderer := &Writer{
ParentColors: []*cachedColor{tc.Parent}, ParentColors: []*Colors{tc.Parent},
Colors: tc.Colors, Colors: tc.Colors,
TerminalBackground: tc.TerminalBackground, TerminalBackground: tc.TerminalBackground,
AnsiColors: &DefaultColors{}, AnsiColors: &DefaultColors{},
@ -210,37 +210,37 @@ func TestWriteLength(t *testing.T) {
Case string Case string
Expected int Expected int
Input string Input string
Colors *cachedColor Colors *Colors
}{ }{
{ {
Case: "Bold", Case: "Bold",
Input: "<b>test</b>", Input: "<b>test</b>",
Expected: 4, Expected: 4,
Colors: &cachedColor{Foreground: "black", Background: ParentBackground}, Colors: &Colors{Foreground: "black", Background: ParentBackground},
}, },
{ {
Case: "Bold with color override", Case: "Bold with color override",
Input: "<b><#ffffff>test</></b>", Input: "<b><#ffffff>test</></b>",
Expected: 4, Expected: 4,
Colors: &cachedColor{Foreground: "black", Background: ParentBackground}, Colors: &Colors{Foreground: "black", Background: ParentBackground},
}, },
{ {
Case: "Bold with color override and link", Case: "Bold with color override and link",
Input: "<b><#ffffff>test</></b> [url](https://example.com)", Input: "<b><#ffffff>test</></b> [url](https://example.com)",
Expected: 8, Expected: 8,
Colors: &cachedColor{Foreground: "black", Background: ParentBackground}, Colors: &Colors{Foreground: "black", Background: ParentBackground},
}, },
{ {
Case: "Bold with color override and link and leading/trailing spaces", Case: "Bold with color override and link and leading/trailing spaces",
Input: " <b><#ffffff>test</></b> [url](https://example.com) ", Input: " <b><#ffffff>test</></b> [url](https://example.com) ",
Expected: 10, Expected: 10,
Colors: &cachedColor{Foreground: "black", Background: ParentBackground}, Colors: &Colors{Foreground: "black", Background: ParentBackground},
}, },
} }
for _, tc := range cases { for _, tc := range cases {
renderer := &Writer{ renderer := &Writer{
ParentColors: []*cachedColor{}, ParentColors: []*Colors{},
Colors: tc.Colors, Colors: tc.Colors,
AnsiColors: &DefaultColors{}, AnsiColors: &DefaultColors{},
} }

View file

@ -10,13 +10,13 @@ import (
"github.com/gookit/color" "github.com/gookit/color"
) )
// Colors is the interface that wraps AnsiColorFromString method. // ColorString is the interface that wraps ToColor method.
// //
// AnsiColorFromString gets the ANSI color code for a given color string. // ToColor gets the ANSI color code for a given color string.
// This can include a valid hex color in the format `#FFFFFF`, // This can include a valid hex color in the format `#FFFFFF`,
// but also a name of one of the first 16 ANSI colors like `lightBlue`. // but also a name of one of the first 16 ANSI colors like `lightBlue`.
type Colors interface { type ColorString interface {
AnsiColorFromString(colorString string, isBackground bool) Color ToColor(colorString string, isBackground bool) Color
} }
// Color is an ANSI color code ready to be printed to the console. // Color is an ANSI color code ready to be printed to the console.
@ -48,7 +48,7 @@ func (c Color) ToForeground() Color {
return c return c
} }
func MakeColors(palette Palette, cacheEnabled bool, accentColor string, env platform.Environment) (colors Colors) { func MakeColors(palette Palette, cacheEnabled bool, accentColor string, env platform.Environment) (colors ColorString) {
defaultColors := &DefaultColors{} defaultColors := &DefaultColors{}
defaultColors.SetAccentColor(env, accentColor) defaultColors.SetAccentColor(env, accentColor)
colors = defaultColors colors = defaultColors
@ -67,7 +67,7 @@ type RGB struct {
// DefaultColors is the default AnsiColors implementation. // DefaultColors is the default AnsiColors implementation.
type DefaultColors struct { type DefaultColors struct {
accent *cachedColor accent *Colors
} }
var ( var (
@ -98,7 +98,7 @@ const (
backgroundIndex = 1 backgroundIndex = 1
) )
func (d *DefaultColors) AnsiColorFromString(colorString string, isBackground bool) Color { func (d *DefaultColors) ToColor(colorString string, isBackground bool) Color {
if len(colorString) == 0 { if len(colorString) == 0 {
return emptyColor return emptyColor
} }
@ -157,24 +157,24 @@ func IsAnsiColorName(colorString string) bool {
// PaletteColors is the AnsiColors Decorator that uses the Palette to do named color // PaletteColors is the AnsiColors Decorator that uses the Palette to do named color
// lookups before ANSI color code generation. // lookups before ANSI color code generation.
type PaletteColors struct { type PaletteColors struct {
ansiColors Colors ansiColors ColorString
palette Palette palette Palette
} }
func (p *PaletteColors) AnsiColorFromString(colorString string, isBackground bool) Color { func (p *PaletteColors) ToColor(colorString string, isBackground bool) Color {
paletteColor, err := p.palette.ResolveColor(colorString) paletteColor, err := p.palette.ResolveColor(colorString)
if err != nil { if err != nil {
return emptyColor return emptyColor
} }
ansiColor := p.ansiColors.AnsiColorFromString(paletteColor, isBackground) ansiColor := p.ansiColors.ToColor(paletteColor, isBackground)
return ansiColor return ansiColor
} }
// CachedColors is the AnsiColors Decorator that does simple color lookup caching. // CachedColors is the AnsiColors Decorator that does simple color lookup caching.
// AnsiColorFromString calls are cheap, but not free, and having a simple cache in // ToColor calls are cheap, but not free, and having a simple cache in
// has measurable positive effect on performance. // has measurable positive effect on performance.
type CachedColors struct { type CachedColors struct {
ansiColors Colors ansiColors ColorString
colorCache map[cachedColorKey]Color colorCache map[cachedColorKey]Color
} }
@ -183,7 +183,7 @@ type cachedColorKey struct {
isBackground bool isBackground bool
} }
func (c *CachedColors) AnsiColorFromString(colorString string, isBackground bool) Color { func (c *CachedColors) ToColor(colorString string, isBackground bool) Color {
if c.colorCache == nil { if c.colorCache == nil {
c.colorCache = make(map[cachedColorKey]Color) c.colorCache = make(map[cachedColorKey]Color)
} }
@ -191,7 +191,7 @@ func (c *CachedColors) AnsiColorFromString(colorString string, isBackground bool
if ansiColor, hit := c.colorCache[key]; hit { if ansiColor, hit := c.colorCache[key]; hit {
return ansiColor return ansiColor
} }
ansiColor := c.ansiColors.AnsiColorFromString(colorString, isBackground) ansiColor := c.ansiColors.ToColor(colorString, isBackground)
c.colorCache[key] = ansiColor c.colorCache[key] = ansiColor
return ansiColor return ansiColor
} }

View file

@ -26,7 +26,7 @@ func TestGetAnsiFromColorString(t *testing.T) {
} }
for _, tc := range cases { for _, tc := range cases {
ansiColors := &DefaultColors{} ansiColors := &DefaultColors{}
ansiColor := ansiColors.AnsiColorFromString(tc.Color, tc.Background) ansiColor := ansiColors.ToColor(tc.Color, tc.Background)
assert.Equal(t, tc.Expected, ansiColor, tc.Case) assert.Equal(t, tc.Expected, ansiColor, tc.Case)
} }
} }

View file

@ -12,8 +12,8 @@ func (d *DefaultColors) SetAccentColor(env platform.Environment, defaultColor st
if len(defaultColor) == 0 { if len(defaultColor) == 0 {
return return
} }
d.accent = &cachedColor{ d.accent = &Colors{
Foreground: string(d.AnsiColorFromString(defaultColor, false)), Foreground: string(d.ToColor(defaultColor, false)),
Background: string(d.AnsiColorFromString(defaultColor, true)), Background: string(d.ToColor(defaultColor, true)),
} }
} }

View file

@ -27,15 +27,15 @@ func GetAccentColor(env platform.Environment) (*RGB, error) {
func (d *DefaultColors) SetAccentColor(env platform.Environment, defaultColor string) { func (d *DefaultColors) SetAccentColor(env platform.Environment, defaultColor string) {
rgb, err := GetAccentColor(env) rgb, err := GetAccentColor(env)
if err != nil { if err != nil {
d.accent = &cachedColor{ d.accent = &Colors{
Foreground: string(d.AnsiColorFromString(defaultColor, false)), Foreground: string(d.ToColor(defaultColor, false)),
Background: string(d.AnsiColorFromString(defaultColor, true)), Background: string(d.ToColor(defaultColor, true)),
} }
return return
} }
foreground := color.RGB(rgb.R, rgb.G, rgb.B, false) foreground := color.RGB(rgb.R, rgb.G, rgb.B, false)
background := color.RGB(rgb.R, rgb.G, rgb.B, true) background := color.RGB(rgb.R, rgb.G, rgb.B, true)
d.accent = &cachedColor{ d.accent = &Colors{
Foreground: foreground.String(), Foreground: foreground.String(),
Background: background.String(), Background: background.String(),
} }

10
src/ansi/cycle.go Normal file
View file

@ -0,0 +1,10 @@
package ansi
type Cycle []*Colors
func PopColors(array []*Colors) (*Colors, Cycle) {
if len(array) == 0 {
return nil, array
}
return array[0], array[1:]
}

View file

@ -55,11 +55,13 @@ type Block struct {
writer *ansi.Writer writer *ansi.Writer
activeSegment *Segment activeSegment *Segment
previousActiveSegment *Segment previousActiveSegment *Segment
cycle *ansi.Cycle
} }
func (b *Block) Init(env platform.Environment, writer *ansi.Writer) { func (b *Block) Init(env platform.Environment, writer *ansi.Writer, cycle *ansi.Cycle) {
b.env = env b.env = env
b.writer = writer b.writer = writer
b.cycle = cycle
b.executeSegmentLogic() b.executeSegmentLogic()
} }
@ -130,6 +132,11 @@ func (b *Block) RenderSegments() (string, int) {
if !segment.Enabled && segment.style() != Accordion { if !segment.Enabled && segment.style() != Accordion {
continue continue
} }
if b.cycle != nil && len(*b.cycle) > 0 {
colors, cycle := ansi.PopColors(*b.cycle)
b.cycle = &cycle
segment.colors = colors
}
b.setActiveSegment(segment) b.setActiveSegment(segment)
b.renderActiveSegment() b.renderActiveSegment()
} }

View file

@ -47,6 +47,7 @@ type Config struct {
DebugPrompt *Segment `json:"debug_prompt,omitempty"` DebugPrompt *Segment `json:"debug_prompt,omitempty"`
Palette ansi.Palette `json:"palette,omitempty"` Palette ansi.Palette `json:"palette,omitempty"`
Palettes *ansi.Palettes `json:"palettes,omitempty"` Palettes *ansi.Palettes `json:"palettes,omitempty"`
Cycle ansi.Cycle `json:"cycle,omitempty"`
PWD string `json:"pwd,omitempty"` PWD string `json:"pwd,omitempty"`
// Deprecated // Deprecated
@ -63,7 +64,7 @@ type Config struct {
// MakeColors creates instance of AnsiColors to use in AnsiWriter according to // MakeColors creates instance of AnsiColors to use in AnsiWriter according to
// environment and configuration. // environment and configuration.
func (cfg *Config) MakeColors() ansi.Colors { func (cfg *Config) MakeColors() ansi.ColorString {
cacheDisabled := cfg.env.Getenv("OMP_CACHE_DISABLED") == "1" cacheDisabled := cfg.env.Getenv("OMP_CACHE_DISABLED") == "1"
return ansi.MakeColors(cfg.getPalette(), !cacheDisabled, cfg.AccentColor, cfg.env) return ansi.MakeColors(cfg.getPalette(), !cacheDisabled, cfg.AccentColor, cfg.env)
} }

View file

@ -21,6 +21,7 @@ type Engine struct {
currentLineLength int currentLineLength int
rprompt string rprompt string
rpromptLength int rpromptLength int
cycle *ansi.Cycle
} }
func (e *Engine) write(text string) { func (e *Engine) write(text string) {
@ -57,6 +58,8 @@ func (e *Engine) canWriteRightBlock(rprompt bool) bool {
} }
func (e *Engine) PrintPrimary() string { func (e *Engine) PrintPrimary() string {
// cache a pointer to the color cycle
e.cycle = &e.Config.Cycle
for _, block := range e.Config.Blocks { for _, block := range e.Config.Blocks {
e.renderBlock(block) e.renderBlock(block)
} }
@ -156,7 +159,7 @@ func (e *Engine) renderBlock(block *Block) {
if e.Env.Shell() == shell.BASH && (block.Type == RPrompt || block.Alignment == Right) { if e.Env.Shell() == shell.BASH && (block.Type == RPrompt || block.Alignment == Right) {
block.InitPlain(e.Env, e.Config) block.InitPlain(e.Env, e.Config)
} else { } else {
block.Init(e.Env, e.Writer) block.Init(e.Env, e.Writer, e.cycle)
} }
if !block.Enabled() { if !block.Enabled() {
return return
@ -243,9 +246,11 @@ func (e *Engine) PrintDebug(startTime time.Time, version string) string {
duration: time.Since(titleStartTime), duration: time.Since(titleStartTime),
} }
segmentTimings = append(segmentTimings, segmentTiming) segmentTimings = append(segmentTimings, segmentTiming)
// cache a pointer to the color cycle
e.cycle = &e.Config.Cycle
// loop each segments of each blocks // loop each segments of each blocks
for _, block := range e.Config.Blocks { for _, block := range e.Config.Blocks {
block.Init(e.Env, e.Writer) block.Init(e.Env, e.Writer, e.cycle)
longestSegmentName, timings := block.Debug() longestSegmentName, timings := block.Debug()
segmentTimings = append(segmentTimings, timings...) segmentTimings = append(segmentTimings, timings...)
if longestSegmentName > largestSegmentNameLength { if longestSegmentName > largestSegmentNameLength {
@ -344,7 +349,7 @@ func (e *Engine) PrintTooltip(tip string) string {
} }
switch e.Env.Shell() { switch e.Env.Shell() {
case shell.ZSH, shell.CMD, shell.FISH, shell.GENERIC: case shell.ZSH, shell.CMD, shell.FISH, shell.GENERIC:
block.Init(e.Env, e.Writer) block.Init(e.Env, e.Writer, nil)
if !block.Enabled() { if !block.Enabled() {
return "" return ""
} }
@ -453,7 +458,7 @@ func (e *Engine) PrintRPrompt() string {
if block == nil { if block == nil {
return "" return ""
} }
block.Init(e.Env, e.Writer) block.Init(e.Env, e.Writer, e.cycle)
if !block.Enabled() { if !block.Enabled() {
return "" return ""
} }

View file

@ -7,6 +7,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/jandedobbeleer/oh-my-posh/src/ansi"
"github.com/jandedobbeleer/oh-my-posh/src/platform" "github.com/jandedobbeleer/oh-my-posh/src/platform"
"github.com/jandedobbeleer/oh-my-posh/src/properties" "github.com/jandedobbeleer/oh-my-posh/src/properties"
"github.com/jandedobbeleer/oh-my-posh/src/segments" "github.com/jandedobbeleer/oh-my-posh/src/segments"
@ -39,12 +40,12 @@ type Segment struct {
MaxWidth int `json:"max_width,omitempty"` MaxWidth int `json:"max_width,omitempty"`
MinWidth int `json:"min_width,omitempty"` MinWidth int `json:"min_width,omitempty"`
Enabled bool `json:"-"`
colors *ansi.Colors
env platform.Environment env platform.Environment
writer SegmentWriter writer SegmentWriter
Enabled bool `json:"-"`
text string text string
backgroundCache string
foregroundCache string
styleCache SegmentStyle styleCache SegmentStyle
} }
@ -360,17 +361,23 @@ func (segment *Segment) shouldInvokeWithTip(tip string) bool {
} }
func (segment *Segment) foreground() string { func (segment *Segment) foreground() string {
if len(segment.foregroundCache) == 0 { if segment.colors == nil {
segment.foregroundCache = segment.ForegroundTemplates.FirstMatch(segment.writer, segment.env, segment.Foreground) segment.colors = &ansi.Colors{}
} }
return segment.foregroundCache if len(segment.colors.Foreground) == 0 {
segment.colors.Foreground = segment.ForegroundTemplates.FirstMatch(segment.writer, segment.env, segment.Foreground)
}
return segment.colors.Foreground
} }
func (segment *Segment) background() string { func (segment *Segment) background() string {
if len(segment.backgroundCache) == 0 { if segment.colors == nil {
segment.backgroundCache = segment.BackgroundTemplates.FirstMatch(segment.writer, segment.env, segment.Background) segment.colors = &ansi.Colors{}
} }
return segment.backgroundCache if len(segment.colors.Background) == 0 {
segment.colors.Background = segment.BackgroundTemplates.FirstMatch(segment.writer, segment.env, segment.Background)
}
return segment.colors.Background
} }
func (segment *Segment) mapSegmentWithWriter(env platform.Environment) error { func (segment *Segment) mapSegmentWithWriter(env platform.Environment) error {

View file

@ -3063,6 +3063,22 @@
} }
} }
}, },
"cycle": {
"type": "array",
"title": "List of settings to cycle through segment by segment",
"description": "https://ohmyposh.dev/docs/configuration/cycle",
"default": [],
"items": {
"properties": {
"foreground": {
"$ref": "#/definitions/color"
},
"background": {
"$ref": "#/definitions/color"
}
}
}
},
"accent_color": { "accent_color": {
"title": "Accent color", "title": "Accent color",
"$ref": "#/definitions/color" "$ref": "#/definitions/color"

View file

@ -236,6 +236,32 @@ you could also add `frappe` as the default `palette`, given that one is used as
the `template` resolves to. In case no match is available and no `palette` is defined, it will also fallback to `transparent` the `template` resolves to. In case no match is available and no `palette` is defined, it will also fallback to `transparent`
for any palette color reference in templates/colors. for any palette color reference in templates/colors.
## Cycle
When you want to display the same **sequence of colors** (background and foreground) regardless of which segments are active, you can
make use of the cycle property. This property is a list of colors which are used one after the other. If there's nothing in the list
anymore and your prompt would still render additional segments, the segment's colors are being used. A defined cycle always gets
precendence over everything else.
```json
...
"cycle": [
{
"background": "p:blue",
"foreground": "p:white"
},
{
"background": "p:green",
"foreground": "p:black"
},
{
"background": "p:orange",
"foreground": "p:white"
}
],
...
```
[hexcolors]: https://htmlcolorcodes.com/color-chart/material-design-color-chart/ [hexcolors]: https://htmlcolorcodes.com/color-chart/material-design-color-chart/
[ansicolors]: https://htmlcolorcodes.com/color-chart/material-design-color-chart/ [ansicolors]: https://htmlcolorcodes.com/color-chart/material-design-color-chart/
[git]: /docs/segments/git [git]: /docs/segments/git