From 911fb9543ef85ca7c12f6182bf6690c643071fbb Mon Sep 17 00:00:00 2001 From: Oliver <480930+rivo@users.noreply.github.com> Date: Sat, 14 Apr 2018 00:05:25 +0200 Subject: [PATCH] Added Escape(), ANSIIWriter(), and TranslateANSII(). Resolves #84, resolves #24 --- README.md | 3 + ansii.go | 237 ++++++++++++++++++++++++++++++++++++++++++++++++++++ doc.go | 8 +- textview.go | 59 +++++++------ util.go | 151 +++++++++++++++++++-------------- 5 files changed, 365 insertions(+), 93 deletions(-) create mode 100644 ansii.go diff --git a/README.md b/README.md index 31e7e95..eca7824 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,9 @@ Add your issue here on GitHub. Feel free to get in touch if you have any questio (There are no corresponding tags in the project. I only keep such a history in this README.) +- v0.14 (2018-04-13) + - Added an `Escape()` function which keep strings like color or region tags from being recognized as such. + - Added `ANSIIWriter()` and `TranslateANSII()` which convert ANSII escape sequences to `tview` color tags. - v0.13 (2018-04-01) - Added background colors and text attributes to color tags. - v0.12 (2018-03-13) diff --git a/ansii.go b/ansii.go new file mode 100644 index 0000000..0ce3d4a --- /dev/null +++ b/ansii.go @@ -0,0 +1,237 @@ +package tview + +import ( + "bytes" + "fmt" + "io" + "strconv" + "strings" +) + +// The states of the ANSII escape code parser. +const ( + ansiiText = iota + ansiiEscape + ansiiSubstring + ansiiControlSequence +) + +// ansii is a io.Writer which translates ANSII escape codes into tview color +// tags. +type ansii struct { + io.Writer + + // Reusable buffers. + buffer *bytes.Buffer // The entire output text of one Write(). + csiParameter, csiIntermediate *bytes.Buffer // Partial CSI strings. + + // The current state of the parser. One of the ansii constants. + state int +} + +// ANSIIWriter returns an io.Writer which translates any ANSII escape codes +// written to it into tview color tags. Other escape codes don't have an effect +// and are simply removed. The translated text is written to the provided +// writer. +func ANSIIWriter(writer io.Writer) io.Writer { + return &ansii{ + Writer: writer, + buffer: new(bytes.Buffer), + csiParameter: new(bytes.Buffer), + csiIntermediate: new(bytes.Buffer), + state: ansiiText, + } +} + +// Write parses the given text as a string of runes, translates ANSII escape +// codes to color tags and writes them to the output writer. +func (a *ansii) Write(text []byte) (int, error) { + defer func() { + a.buffer.Reset() + }() + + for _, r := range string(text) { + switch a.state { + + // We just entered an escape sequence. + case ansiiEscape: + switch r { + case '[': // Control Sequence Introducer. + a.csiParameter.Reset() + a.csiIntermediate.Reset() + a.state = ansiiControlSequence + case 'c': // Reset. + fmt.Fprint(a.buffer, "[-:-:-]") + a.state = ansiiText + case 'P', ']', 'X', '^', '_': // Substrings and commands. + a.state = ansiiSubstring + default: // Ignore. + a.state = ansiiText + } + + // CSI Sequences. + case ansiiControlSequence: + switch { + case r >= 0x30 && r <= 0x3f: // Parameter bytes. + if _, err := a.csiParameter.WriteRune(r); err != nil { + return 0, err + } + case r >= 0x20 && r <= 0x2f: // Intermediate bytes. + if _, err := a.csiIntermediate.WriteRune(r); err != nil { + return 0, err + } + case r >= 0x40 && r <= 0x7e: // Final byte. + switch r { + case 'E': // Next line. + count, _ := strconv.Atoi(a.csiParameter.String()) + if count == 0 { + count = 1 + } + fmt.Fprint(a.buffer, strings.Repeat("\n", count)) + case 'm': // Select Graphic Rendition. + var ( + background, foreground, attributes string + clearAttributes bool + ) + fields := strings.Split(a.csiParameter.String(), ";") + if len(fields) == 0 || len(fields) == 1 && fields[0] == "0" { + // Reset. + if _, err := a.buffer.WriteString("[-:-:-]"); err != nil { + return 0, err + } + break + } + lookupColor := func(colorNumber int, bright bool) string { + if colorNumber < 0 || colorNumber > 7 { + return "black" + } + if bright { + colorNumber += 8 + } + return [...]string{ + "black", + "red", + "green", + "yellow", + "blue", + "darkmagenta", + "darkcyan", + "white", + "#7f7f7f", + "#ff0000", + "#00ff00", + "#ffff00", + "#5c5cff", + "#ff00ff", + "#00ffff", + "#ffffff", + }[colorNumber] + } + for index, field := range fields { + switch field { + case "1", "01": + attributes += "b" + case "2", "02": + attributes += "d" + case "4", "04": + attributes += "u" + case "5", "05": + attributes += "l" + case "7", "07": + attributes += "7" + case "22", "24", "25", "27": + clearAttributes = true + case "30", "31", "32", "33", "34", "35", "36", "37": + colorNumber, _ := strconv.Atoi(field) + foreground = lookupColor(colorNumber-30, false) + case "40", "41", "42", "43", "44", "45", "46", "47": + colorNumber, _ := strconv.Atoi(field) + background = lookupColor(colorNumber-40, false) + case "90", "91", "92", "93", "94", "95", "96", "97": + colorNumber, _ := strconv.Atoi(field) + foreground = lookupColor(colorNumber-90, true) + case "100", "101", "102", "103", "104", "105", "106", "107": + colorNumber, _ := strconv.Atoi(field) + background = lookupColor(colorNumber-100, true) + case "38", "48": + var color string + if len(fields) > index+1 { + if fields[index+1] == "5" && len(fields) > index+2 { // 8-bit colors. + colorNumber, _ := strconv.Atoi(fields[index+2]) + if colorNumber <= 7 { + color = lookupColor(colorNumber, false) + } else if colorNumber <= 15 { + color = lookupColor(colorNumber, true) + } else if colorNumber <= 231 { + red := (colorNumber - 16) / 36 + green := ((colorNumber - 16) / 6) % 6 + blue := (colorNumber - 16) % 6 + color = fmt.Sprintf("%02x%02x%02x", 255*red/5, 255*green/5, 255*blue/5) + } else if colorNumber <= 255 { + grey := 255 * (colorNumber - 232) / 23 + color = fmt.Sprintf("%02x%02x%02x", grey, grey, grey) + } + } else if fields[index+1] == "2" && len(fields) > index+4 { // 24-bit colors. + red, _ := strconv.Atoi(fields[index+2]) + green, _ := strconv.Atoi(fields[index+3]) + blue, _ := strconv.Atoi(fields[index+4]) + color = fmt.Sprintf("%02x%02x%02x", red, green, blue) + } + } + if len(color) > 0 { + if field == "38" { + foreground = color + } else { + background = color + } + } + } + } + if len(attributes) > 0 || clearAttributes { + attributes = ":" + attributes + } + if len(foreground) > 0 || len(background) > 0 || len(attributes) > 0 { + fmt.Fprintf(a.buffer, "[%s:%s%s]", foreground, background, attributes) + } + } + a.state = ansiiText + default: // Undefined byte. + a.state = ansiiText // Abort CSI. + } + + // We just entered a substring/command sequence. + case ansiiSubstring: + if r == 27 { // Most likely the end of the substring. + a.state = ansiiEscape + } // Ignore all other characters. + + // "ansiiText" and all others. + default: + if r == 27 { + // This is the start of an escape sequence. + a.state = ansiiEscape + } else { + // Just a regular rune. Send to buffer. + if _, err := a.buffer.WriteRune(r); err != nil { + return 0, err + } + } + } + } + + // Write buffer to target writer. + n, err := a.buffer.WriteTo(a.Writer) + if err != nil { + return int(n), err + } + return len(text), nil +} + +// TranslateANSII replaces ANSII escape sequences found in the provided string +// with tview's color tags and returns the resulting string. +func TranslateANSII(text string) string { + var buffer bytes.Buffer + writer := ANSIIWriter(&buffer) + writer.Write([]byte(text)) + return buffer.String() +} diff --git a/doc.go b/doc.go index 20e7634..84cf67d 100644 --- a/doc.go +++ b/doc.go @@ -85,8 +85,8 @@ tag is as follows: Each of the three fields can be left blank and trailing fields can be ommitted. (Empty square brackets "[]", however, are not considered color tags.) Colors -that are not specified will be left unchanged. (If the flags field is indicated -by a colon but left empty, it will reset any flags.) +that are not specified will be left unchanged. A field with just a dash ("-") +means "reset to default". You can specify the following flags (some flags may not be supported by your terminal): @@ -104,7 +104,9 @@ Examples: [:red]Red background, text color unchanged [yellow::u]Yellow text underlined [::bl]Bold, blinking text - [::]Colors unchanged, flags reset + [::-]Colors unchanged, flags reset + [-]Reset foreground color + [-:-:-]Reset everything [:]No effect []Not a valid color tag, will print square brackets as they are diff --git a/textview.go b/textview.go index 98eb2bb..a0c2413 100644 --- a/textview.go +++ b/textview.go @@ -18,13 +18,14 @@ var TabSize = 4 // textViewIndex contains information about each line displayed in the text // view. type textViewIndex struct { - Line int // The index into the "buffer" variable. - Pos int // The index into the "buffer" string (byte position). - NextPos int // The (byte) index of the next character in this buffer line. - Width int // The screen width of this line. - Style tcell.Style // The starting style. - OverwriteAttr bool // The starting flag indicating if style attributes should be overwritten. - Region string // The starting region ID. + Line int // The index into the "buffer" variable. + Pos int // The index into the "buffer" string (byte position). + NextPos int // The (byte) index of the next character in this buffer line. + Width int // The screen width of this line. + ForegroundColor string // The starting foreground color ("" = don't change, "-" = reset). + BackgroundColor string // The starting background color ("" = don't change, "-" = reset). + Attributes string // The starting attributes ("" = don't change, "-" = reset). + Region string // The starting region ID. } // TextView is a box which displays text. It implements the io.Writer interface @@ -500,8 +501,7 @@ func (t *TextView) reindexBuffer(width int) { // Initial states. regionID := "" - var highlighted, overwriteAttr bool - style := tcell.StyleDefault.Foreground(t.textColor) + var highlighted bool // Go through each line in the buffer. for bufferIndex, str := range t.buffer { @@ -558,14 +558,18 @@ func (t *TextView) reindexBuffer(width int) { } // Create index from split lines. - var originalPos, colorPos, regionPos, escapePos int + var ( + originalPos, colorPos, regionPos, escapePos int + foregroundColor, backgroundColor, attributes string + ) for _, splitLine := range splitLines { line := &textViewIndex{ - Line: bufferIndex, - Pos: originalPos, - Style: style, - OverwriteAttr: overwriteAttr, - Region: regionID, + Line: bufferIndex, + Pos: originalPos, + ForegroundColor: foregroundColor, + BackgroundColor: backgroundColor, + Attributes: attributes, + Region: regionID, } // Shift original position with tags. @@ -574,7 +578,7 @@ func (t *TextView) reindexBuffer(width int) { if colorPos < len(colorTagIndices) && colorTagIndices[colorPos][0] <= originalPos+lineLength { // Process color tags. originalPos += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0] - style, overwriteAttr = styleFromTag(style, overwriteAttr, colorTags[colorPos]) + foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[colorPos]) colorPos++ } else if regionPos < len(regionIndices) && regionIndices[regionPos][0] <= originalPos+lineLength { // Process region tags. @@ -712,6 +716,7 @@ func (t *TextView) Draw(screen tcell.Screen) { } // Draw the buffer. + defaultStyle := tcell.StyleDefault.Foreground(t.textColor) for line := t.lineOffset; line < len(t.index); line++ { // Are we done? if line-t.lineOffset >= height { @@ -721,8 +726,9 @@ func (t *TextView) Draw(screen tcell.Screen) { // Get the text for this line. index := t.index[line] text := t.buffer[index.Line][index.Pos:index.NextPos] - style := index.Style - overwriteAttr := index.OverwriteAttr + foregroundColor := index.ForegroundColor + backgroundColor := index.BackgroundColor + attributes := index.Attributes regionID := index.Region // Get color tags. @@ -768,7 +774,7 @@ func (t *TextView) Draw(screen tcell.Screen) { // Get the color. if currentTag < len(colorTags) && pos >= colorTagIndices[currentTag][0] && pos < colorTagIndices[currentTag][1] { if pos == colorTagIndices[currentTag][1]-1 { - style, overwriteAttr = styleFromTag(style, overwriteAttr, colorTags[currentTag]) + foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[currentTag]) currentTag++ } continue @@ -809,8 +815,12 @@ func (t *TextView) Draw(screen tcell.Screen) { break } + // Mix the existing style with the new style. + _, _, existingStyle, _ := screen.GetContent(x+posX, y+line-t.lineOffset) + _, background, _ := existingStyle.Decompose() + style := overlayStyle(background, defaultStyle, foregroundColor, backgroundColor, attributes) + // Do we highlight this character? - finalStyle := style var highlighted bool if len(regionID) > 0 { if _, ok := t.highlights[regionID]; ok { @@ -818,7 +828,7 @@ func (t *TextView) Draw(screen tcell.Screen) { } } if highlighted { - fg, bg, _ := finalStyle.Decompose() + fg, bg, _ := style.Decompose() if bg == tcell.ColorDefault { r, g, b := fg.RGB() c := colorful.Color{R: float64(r) / 255, G: float64(g) / 255, B: float64(b) / 255} @@ -829,15 +839,12 @@ func (t *TextView) Draw(screen tcell.Screen) { bg = tcell.ColorBlack } } - finalStyle = style.Background(fg).Foreground(bg) - } else { - _, _, existingStyle, _ := screen.GetContent(x+posX, y+line-t.lineOffset) - finalStyle = overlayStyle(existingStyle, style, overwriteAttr) + style = style.Background(fg).Foreground(bg) } // Draw the character. for offset := 0; offset < chWidth; offset++ { - screen.SetContent(x+posX+offset, y+line-t.lineOffset, ch, nil, finalStyle) + screen.SetContent(x+posX+offset, y+line-t.lineOffset, ch, nil, style) } // Advance. diff --git a/util.go b/util.go index b88bdf0..0fa0cd8 100644 --- a/util.go +++ b/util.go @@ -1,6 +1,7 @@ package tview import ( + "fmt" "math" "regexp" "strconv" @@ -104,7 +105,7 @@ var joints = map[string]rune{ // Common regular expressions. var ( - colorPattern = regexp.MustCompile(`\[([a-zA-Z]+|#[0-9a-zA-Z]{6})?(:([a-zA-Z]+|#[0-9a-zA-Z]{6})?(:([lbdru]+))?)?\]`) + colorPattern = regexp.MustCompile(`\[([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([lbdru]+|\-)?)?)?\]`) regionPattern = regexp.MustCompile(`\["([a-zA-Z0-9_,;: \-\.]*)"\]`) escapePattern = regexp.MustCompile(`\[([a-zA-Z0-9_,;: \-\."#]+)\[(\[*)\]`) nonEscapePattern = regexp.MustCompile(`(\[[a-zA-Z0-9_,;: \-\."#]+\[*)\]`) @@ -158,35 +159,72 @@ func init() { } } -// styleFromTag takes the given style and modifies it based on the substrings -// extracted by the regular expression for color tags. The new style is returned -// as well as the flag indicating if any style attributes were explicitly -// specified (whose original value is also returned). -func styleFromTag(style tcell.Style, overwriteAttr bool, tagSubstrings []string) (tcell.Style, bool) { - // Colors. +// styleFromTag takes the given style, defined by a foreground color (fgColor), +// a background color (bgColor), and style attributes, and modifies it based on +// the substrings (tagSubstrings) extracted by the regular expression for color +// tags. The new colors and attributes are returned where empty strings mean +// "don't modify" and a dash ("-") means "reset to default". +func styleFromTag(fgColor, bgColor, attributes string, tagSubstrings []string) (newFgColor, newBgColor, newAttributes string) { if tagSubstrings[colorForegroundPos] != "" { color := tagSubstrings[colorForegroundPos] - if color == "" { - style = style.Foreground(tcell.ColorDefault) - } else { - style = style.Foreground(tcell.GetColor(color)) - } - } - if tagSubstrings[colorBackgroundPos-1] != "" { - color := tagSubstrings[colorBackgroundPos] - if color == "" { - style = style.Background(tcell.ColorDefault) - } else { - style = style.Background(tcell.GetColor(color)) + if color == "-" { + fgColor = "-" + } else if color != "" { + fgColor = color } } - // Flags. - specified := tagSubstrings[colorFlagPos-1] != "" - if specified { - overwriteAttr = true + if tagSubstrings[colorBackgroundPos-1] != "" { + color := tagSubstrings[colorBackgroundPos] + if color == "-" { + bgColor = "-" + } else if color != "" { + bgColor = color + } + } + + if tagSubstrings[colorFlagPos-1] != "" { + flags := tagSubstrings[colorFlagPos] + if flags == "-" { + attributes = "-" + } else if flags != "" { + attributes = flags + } + } + + return fgColor, bgColor, attributes +} + +// overlayStyle mixes a background color with a foreground color (fgColor), +// a (possibly new) background color (bgColor), and style attributes, and +// returns the resulting style. For a definition of the colors and attributes, +// see styleFromTag(). Reset instructions cause the corresponding part of the +// default style to be used. +func overlayStyle(background tcell.Color, defaultStyle tcell.Style, fgColor, bgColor, attributes string) tcell.Style { + defFg, defBg, defAttr := defaultStyle.Decompose() + style := defaultStyle.Background(background) + + if fgColor == "-" { + style = style.Foreground(defFg) + } else if fgColor != "" { + style = style.Foreground(tcell.GetColor(fgColor)) + } + + if bgColor == "-" { + style = style.Background(defBg) + } else if bgColor != "" { + style = style.Background(tcell.GetColor(bgColor)) + } + + if attributes == "-" { + style = style.Bold(defAttr&tcell.AttrBold > 0) + style = style.Blink(defAttr&tcell.AttrBlink > 0) + style = style.Reverse(defAttr&tcell.AttrReverse > 0) + style = style.Underline(defAttr&tcell.AttrUnderline > 0) + style = style.Dim(defAttr&tcell.AttrDim > 0) + } else if attributes != "" { style = style.Normal() - for _, flag := range tagSubstrings[colorFlagPos] { + for _, flag := range attributes { switch flag { case 'l': style = style.Blink(true) @@ -201,26 +239,7 @@ func styleFromTag(style tcell.Style, overwriteAttr bool, tagSubstrings []string) } } } - return style, overwriteAttr -} -// overlayStyle mixes a bottom and a top style and returns the result. Top -// colors (other than tcell.ColorDefault) overwrite bottom colors. Top -// style attributes overwrite bottom style attributes only if overwriteAttr is -// true. -func overlayStyle(bottom, top tcell.Style, overwriteAttr bool) tcell.Style { - style := bottom - fg, bg, attr := top.Decompose() - if bg != tcell.ColorDefault { - style = style.Background(bg) - } - if fg != tcell.ColorDefault { - style = style.Foreground(fg) - } - if overwriteAttr { - style = style.Normal() - style |= tcell.Style(attr) - } return style } @@ -272,13 +291,12 @@ func decomposeString(text string) (colorIndices [][]int, colors [][]string, esca // Returns the number of actual runes printed (not including color tags) and the // actual width used for the printed runes. func Print(screen tcell.Screen, text string, x, y, maxWidth, align int, color tcell.Color) (int, int) { - return printWithStyle(screen, text, x, y, maxWidth, align, tcell.StyleDefault.Foreground(color), false) + return printWithStyle(screen, text, x, y, maxWidth, align, tcell.StyleDefault.Foreground(color)) } // printWithStyle works like Print() but it takes a style instead of just a -// foreground color. The overwriteAttr indicates whether or not a style's -// additional attributes (see tcell.AttrMask) should be overwritten. -func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, style tcell.Style, overwriteAttr bool) (int, int) { +// foreground color. +func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, style tcell.Style) (int, int) { if maxWidth < 0 { return 0, 0 } @@ -289,17 +307,20 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, // We deal with runes, not with bytes. runes := []rune(strippedText) - // This helper function takes positions for a substring of "runes" and a start - // style and returns the substring with the original tags and the new start - // style. - substring := func(from, to int, style tcell.Style, overwriteAttr bool) (string, tcell.Style, bool) { - var colorPos, escapePos, runePos, startPos int + // This helper function takes positions for a substring of "runes" and returns + // a new string corresponding to this substring, making sure printing that + // substring will observe color tags. + substring := func(from, to int) string { + var ( + colorPos, escapePos, runePos, startPos int + foregroundColor, backgroundColor, attributes string + ) for pos := range text { // Handle color tags. if colorPos < len(colorIndices) && pos >= colorIndices[colorPos][0] && pos < colorIndices[colorPos][1] { if pos == colorIndices[colorPos][1]-1 { if runePos <= from { - style, overwriteAttr = styleFromTag(style, overwriteAttr, colors[colorPos]) + foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos]) } colorPos++ } @@ -319,13 +340,13 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, if runePos == from { startPos = pos } else if runePos >= to { - return text[startPos:pos], style, overwriteAttr + return fmt.Sprintf(`[%s:%s:%s]%s`, foregroundColor, backgroundColor, attributes, text[startPos:pos]) } runePos++ } - return text[startPos:], style, overwriteAttr + return fmt.Sprintf(`[%s:%s:%s]%s`, foregroundColor, backgroundColor, attributes, text[startPos:]) } // We want to reduce everything to AlignLeft. @@ -340,17 +361,16 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, width += w start = index } - text, style, overwriteAttr = substring(start, len(runes), style, overwriteAttr) - return printWithStyle(screen, text, x+maxWidth-width, y, width, AlignLeft, style, overwriteAttr) + return printWithStyle(screen, substring(start, len(runes)), x+maxWidth-width, y, width, AlignLeft, style) } else if align == AlignCenter { width := runewidth.StringWidth(strippedText) if width == maxWidth { // Use the exact space. - return printWithStyle(screen, text, x, y, maxWidth, AlignLeft, style, overwriteAttr) + return printWithStyle(screen, text, x, y, maxWidth, AlignLeft, style) } else if width < maxWidth { // We have more space than we need. half := (maxWidth - width) / 2 - return printWithStyle(screen, text, x+half, y, maxWidth-half, AlignLeft, style, overwriteAttr) + return printWithStyle(screen, text, x+half, y, maxWidth-half, AlignLeft, style) } else { // Chop off runes until we have a perfect fit. var choppedLeft, choppedRight, leftIndex, rightIndex int @@ -366,20 +386,22 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, rightIndex-- } } - text, style, overwriteAttr = substring(leftIndex, rightIndex, style, overwriteAttr) - return printWithStyle(screen, text, x, y, maxWidth, AlignLeft, style, overwriteAttr) + return printWithStyle(screen, substring(leftIndex, rightIndex), x, y, maxWidth, AlignLeft, style) } } // Draw text. drawn := 0 drawnWidth := 0 - var colorPos, escapePos int + var ( + colorPos, escapePos int + foregroundColor, backgroundColor, attributes string + ) for pos, ch := range text { // Handle color tags. if colorPos < len(colorIndices) && pos >= colorIndices[colorPos][0] && pos < colorIndices[colorPos][1] { if pos == colorIndices[colorPos][1]-1 { - style, overwriteAttr = styleFromTag(style, overwriteAttr, colors[colorPos]) + foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos]) colorPos++ } continue @@ -403,7 +425,8 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, // Print the rune. _, _, finalStyle, _ := screen.GetContent(finalX, y) - finalStyle = overlayStyle(finalStyle, style, overwriteAttr) + _, background, _ := finalStyle.Decompose() + finalStyle = overlayStyle(background, style, foregroundColor, backgroundColor, attributes) for offset := 0; offset < chWidth; offset++ { // To avoid undesired effects, we place the same character in all cells. screen.SetContent(finalX+offset, y, ch, nil, finalStyle)