feat(colors): allow nested overrides

This commit is contained in:
Jan De Dobbeleer 2023-09-07 16:57:09 +02:00 committed by Jan De Dobbeleer
parent 20c11453d5
commit 346091feb8
4 changed files with 156 additions and 64 deletions

View file

@ -86,13 +86,12 @@ type Writer struct {
builder strings.Builder
length int
foreground Color
background Color
currentForeground Color
currentBackground Color
runes []rune
transparent bool
invisible bool
foreground Color
background Color
current ColorHistory
runes []rune
transparent bool
invisible bool
shell string
format string
@ -303,7 +302,8 @@ func (w *Writer) Write(background, foreground, text string) {
colorOverride = false
}
if colorOverride {
w.currentBackground, w.currentForeground = w.asAnsiColors(match[BG], match[FG])
w.current.Add(w.asAnsiColors(match[BG], match[FG]))
// w.current.Background(), w.current.Foreground() = w.asAnsiColors(match[BG], match[FG])
}
}
w.writeSegmentColors()
@ -343,8 +343,7 @@ func (w *Writer) Write(background, foreground, text string) {
w.writeEscapedAnsiString(resetStyle.End)
// reset current
w.currentBackground = ""
w.currentForeground = ""
w.current.Pop()
}
func (w *Writer) String() (string, int) {
@ -378,11 +377,11 @@ func (w *Writer) writeSegmentColors() {
// use correct starting colors
bg := w.background
fg := w.foreground
if !w.currentBackground.IsEmpty() {
bg = w.currentBackground
if !w.current.Background().IsEmpty() {
bg = w.current.Background()
}
if !w.currentForeground.IsEmpty() {
fg = w.currentForeground
if !w.current.Foreground().IsEmpty() {
fg = w.current.Foreground()
}
// ignore processing fully tranparent colors
@ -408,45 +407,14 @@ func (w *Writer) writeSegmentColors() {
}
// set current colors
w.currentBackground = bg
w.currentForeground = fg
w.current.Add(bg, fg)
}
func (w *Writer) writeColorOverrides(match map[string]string, background string, i int) int {
position := i
// check color reset first
if match[ANCHOR] == resetStyle.AnchorEnd {
// make sure to reset the colors if needed
position += len([]rune(resetStyle.AnchorEnd)) - 1
// do not reset when colors are identical
if w.currentBackground == w.background && w.currentForeground == w.foreground {
return position
}
// do not restore colors at the end of the string, we print it anyways
if position == len(w.runes)-1 {
return position
}
if w.transparent {
w.writeEscapedAnsiString(transparentEnd)
}
if w.background.IsClear() {
w.writeEscapedAnsiString(backgroundStyle.End)
}
if w.currentBackground != w.background && !w.background.IsClear() {
w.writeEscapedAnsiString(fmt.Sprintf(colorise, w.background))
}
if (w.currentForeground != w.foreground || w.transparent) && !w.foreground.IsClear() {
w.writeEscapedAnsiString(fmt.Sprintf(colorise, w.foreground))
}
w.transparent = false
return position
return w.endColorOverride(position)
}
position += len([]rune(match[ANCHOR])) - 1
@ -465,51 +433,113 @@ func (w *Writer) writeColorOverrides(match map[string]string, background string,
if match[FG] == Transparent && len(match[BG]) == 0 {
match[BG] = background
}
w.currentBackground, w.currentForeground = w.asAnsiColors(match[BG], match[FG])
bg, fg := w.asAnsiColors(match[BG], match[FG])
// ignore processing fully tranparent colors
w.invisible = w.currentForeground.IsTransparent() && w.currentBackground.IsTransparent()
w.invisible = fg.IsTransparent() && bg.IsTransparent()
if w.invisible {
return position
}
// make sure we have colors
if w.currentForeground.IsEmpty() {
w.currentForeground = w.foreground
if fg.IsEmpty() {
fg = w.foreground
}
if w.currentBackground.IsEmpty() {
w.currentBackground = w.background
if bg.IsEmpty() {
bg = w.background
}
if w.currentForeground.IsTransparent() && len(w.TerminalBackground) != 0 {
w.current.Add(bg, fg)
if w.current.Foreground().IsTransparent() && len(w.TerminalBackground) != 0 {
background := w.getAnsiFromColorString(w.TerminalBackground, false)
w.writeEscapedAnsiString(fmt.Sprintf(colorise, background))
w.writeEscapedAnsiString(fmt.Sprintf(colorise, w.currentBackground.ToForeground()))
w.writeEscapedAnsiString(fmt.Sprintf(colorise, w.current.Background().ToForeground()))
return position
}
if w.currentForeground.IsTransparent() && !w.currentBackground.IsTransparent() {
if w.current.Foreground().IsTransparent() && !w.current.Background().IsTransparent() {
w.transparent = true
w.writeEscapedAnsiString(fmt.Sprintf(transparent, w.currentBackground))
w.writeEscapedAnsiString(fmt.Sprintf(transparent, w.current.Background()))
return position
}
if w.currentBackground != w.background {
if w.current.Background() != w.background {
// end the colors in case we have a transparent background
if w.currentBackground.IsTransparent() {
if w.current.Background().IsTransparent() {
w.writeEscapedAnsiString(backgroundEnd)
} else {
w.writeEscapedAnsiString(fmt.Sprintf(colorise, w.currentBackground))
w.writeEscapedAnsiString(fmt.Sprintf(colorise, w.current.Background()))
}
}
if w.currentForeground != w.foreground {
w.writeEscapedAnsiString(fmt.Sprintf(colorise, w.currentForeground))
if w.current.Foreground() != w.foreground {
w.writeEscapedAnsiString(fmt.Sprintf(colorise, w.current.Foreground()))
}
return position
}
func (w *Writer) endColorOverride(position int) int {
// pop the last colors from the stack
defer w.current.Pop()
// make sure to reset the colors if needed
position += len([]rune(resetStyle.AnchorEnd)) - 1
// reset colors to previous when we have > 2 in stack
// - first is always the starting colors used in Write()
// - second is the first override
// as soon as we have more than 2, we can pop the last one
// and print the previous override as it wasn't ended yet
if w.current.Len() >= 3 {
fg := w.current.Foreground()
bg := w.current.Background()
w.current.Pop()
if w.current.Background() != bg {
w.writeEscapedAnsiString(fmt.Sprintf(colorise, w.current.Background()))
}
if w.current.Foreground() != fg {
w.writeEscapedAnsiString(fmt.Sprintf(colorise, w.current.Foreground()))
}
return position
}
// do not reset when colors are identical
if w.current.Background() == w.background && w.current.Foreground() == w.foreground {
return position
}
// do not restore colors at the end of the string, we print it anyways
if position == len(w.runes)-1 {
return position
}
if w.transparent {
w.writeEscapedAnsiString(transparentEnd)
}
if w.background.IsClear() {
w.writeEscapedAnsiString(backgroundStyle.End)
}
if w.current.Background() != w.background && !w.background.IsClear() {
w.writeEscapedAnsiString(fmt.Sprintf(colorise, w.background))
}
if (w.current.Foreground() != w.foreground || w.transparent) && !w.foreground.IsClear() {
w.writeEscapedAnsiString(fmt.Sprintf(colorise, w.foreground))
}
w.transparent = false
return position
}
func (w *Writer) asAnsiColors(background, foreground string) (Color, Color) {
if len(background) == 0 {
background = Background

View file

@ -76,8 +76,9 @@ func (w *Writer) replaceHyperlink(text string) string {
return strings.Replace(text, results["ALL"], linkText, 1)
}
// we only care about the length of the text part
w.length += runewidth.StringWidth(linkText)
// we only care about the length of the actual text part
characters := w.trimAnsi(linkText)
w.length += runewidth.StringWidth(characters)
if w.Plain {
return linkText

View file

@ -195,6 +195,12 @@ func TestWriteANSIColors(t *testing.T) {
Expected: "\x1b[40m\x1b[30mtest\x1b[0m",
Colors: &Colors{Foreground: "black", Background: "white"},
},
{
Case: "Nested override",
Input: "hello, <red>world, <white>rabbit</> hello</>",
Expected: "\x1b[47m\x1b[30mhello, \x1b[31mworld, \x1b[37mrabbit\x1b[31m hello\x1b[0m",
Colors: &Colors{Foreground: "black", Background: "white"},
},
}
for _, tc := range cases {

View file

@ -19,6 +19,61 @@ type ColorString interface {
ToColor(colorString string, isBackground bool, trueColor bool) Color
}
type ColorSet struct {
Foreground Color
Background Color
}
type ColorHistory []*ColorSet
func (c *ColorHistory) Len() int {
return len(*c)
}
func (c *ColorHistory) Add(background, foreground Color) {
colors := &ColorSet{
Foreground: foreground,
Background: background,
}
if c.Len() == 0 {
*c = append(*c, colors)
return
}
last := (*c)[c.Len()-1]
// never add the same colors twice
if last.Foreground == colors.Foreground && last.Background == colors.Background {
return
}
*c = append(*c, colors)
}
func (c *ColorHistory) Pop() {
if c.Len() == 0 {
return
}
*c = (*c)[:c.Len()-1]
}
func (c *ColorHistory) Background() Color {
if c.Len() == 0 {
return emptyColor
}
return (*c)[c.Len()-1].Background
}
func (c *ColorHistory) Foreground() Color {
if c.Len() == 0 {
return emptyColor
}
return (*c)[c.Len()-1].Foreground
}
// Color is an ANSI color code ready to be printed to the console.
// Example: "38;2;255;255;255", "48;2;255;255;255", "31", "95".
type Color string