Added background colors and text attributes to color tags. Resolves #91

This commit is contained in:
Oliver 2018-04-01 21:19:10 +02:00
parent 74643a2db5
commit 83d0a16fb2
7 changed files with 232 additions and 75 deletions

View File

@ -64,6 +64,8 @@ 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.13 (2018-04-01)
- Added background colors and text attributes to color tags.
- v0.12 (2018-03-13)
- Added "suspended mode" to `Application`.
- v0.11 (2018-03-02)

View File

@ -6,7 +6,7 @@ import "github.com/rivo/tview"
func main() {
box := tview.NewBox().
SetBorder(true).
SetTitle("Box Demo")
SetTitle("A [red]c[yellow]o[green]l[darkcyan]o[blue]r[darkmagenta]f[red]u[yellow]l[white] [black:red]c[:yellow]o[:green]l[:darkcyan]o[:blue]r[:darkmagenta]f[:red]u[:yellow]l[white:] [::bu]title")
if err := tview.NewApplication().SetRoot(box, true).Run(); err != nil {
panic(err)
}

View File

@ -7,7 +7,7 @@ import (
"github.com/rivo/tview"
)
const colorsText = `You can use color tags almost everywhere to partially change the color of a string. Simply put a color name or hex string in square brackets to change the following characters' color. H[green]er[white]e i[yellow]s a[darkcyan]n ex[red]amp[white]le. The tags look like this: [red[] [#00ff00[]`
const colorsText = `You can use color tags almost everywhere to partially change the color of a string. Simply put a color name or hex string in square brackets to change the following characters' color. H[green]er[white]e i[yellow]s a[darkcyan]n ex[red]amp[white]le. The [black:red]tags [black:green]look [black:yellow]like [::u]this: [blue:yellow:u[] [#00ff00[]`
// Colors demonstrates how to use colors.
func Colors(nextSlide func()) (title string, content tview.Primitive) {
@ -28,6 +28,6 @@ func Colors(nextSlide func()) (title string, content tview.Primitive) {
}
table.SetBorderPadding(1, 1, 2, 2).
SetBorder(true).
SetTitle("A [red]c[yellow]o[green]l[darkcyan]o[blue]r[darkmagenta]f[red]u[yellow]l[white] title")
return "Colors", Center(73, 19, table)
SetTitle("A [red]c[yellow]o[green]l[darkcyan]o[blue]r[darkmagenta]f[red]u[yellow]l[white] [black:red]c[:yellow]o[:green]l[:darkcyan]o[:blue]r[:darkmagenta]f[:red]u[:yellow]l[white:] [::bu]title")
return "Colors", Center(78, 19, table)
}

38
doc.go
View File

@ -77,13 +77,49 @@ applies to almost everything from box titles, list text, form item labels, to
table cells. In a TextView, this functionality has to be switched on explicitly.
See the TextView documentation for more information.
Color tags may contain not just the foreground (text) color but also the
background color and additional flags. In fact, the full definition of a color
tag is as follows:
[<foreground>:<background>:<flags>]
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 changed. (If the flags field is indicated by a
colon but left empty, it will reset any flags.)
You can specify the following flags (some flags may not be supported by your
terminal):
l: blink
b: bold
d: dim
r: reverse (switch foreground and background color)
u: underline
Examples:
[yellow]Yellow text
[yellow:red]Yellow text on red background
[:red]Red background, text color unchanged
[yellow::u]Yellow text underlined
[::bl]Bold, blinking text
[::]Colors unchanged, flags reset
[:]No effect
[]Not a valid color tag, will print square brackets as they are
In the rare event that you want to display a string such as "[red]" or
"[#00ff1a]" without applying its effect, you need to put an opening square
bracket before the closing square bracket. Examples:
bracket before the closing square bracket. Note that the text inside the
brackets will be matched less strictly than region or colors tags. I.e. any
character that may be used in color or region tags will be recognized. Examples:
[red[] will be output as [red]
["123"[] will be output as ["123"]
[#6aff00[[] will be output as [#6aff00[]
[a#"[[[] will be output as [a#"[[]
[] will be output as [] (see color tags above)
[[] will be output as [[] (not an escaped tag)
Styles

View File

@ -590,7 +590,7 @@ ColumnLoop:
expansion := 0
for _, row := range rows {
if cell := getCell(row, column); cell != nil {
cellWidth := StringWidth(cell.Text)
_, _, _, _, cellWidth := decomposeString(cell.Text)
if cell.MaxWidth > 0 && cell.MaxWidth < cellWidth {
cellWidth = cell.MaxWidth
}

View File

@ -8,6 +8,7 @@ import (
"unicode/utf8"
"github.com/gdamore/tcell"
colorful "github.com/lucasb-eyer/go-colorful"
runewidth "github.com/mattn/go-runewidth"
)
@ -17,12 +18,13 @@ 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.
Color tcell.Color // The starting color.
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.
Style tcell.Style // The starting style.
OverwriteAttr bool // The starting flag indicating if style attributes should be overwritten.
Region string // The starting region ID.
}
// TextView is a box which displays text. It implements the io.Writer interface
@ -498,8 +500,8 @@ func (t *TextView) reindexBuffer(width int) {
// Initial states.
regionID := ""
var highlighted bool
color := t.textColor
var highlighted, overwriteAttr bool
style := tcell.StyleDefault.Foreground(t.textColor)
// Go through each line in the buffer.
for bufferIndex, str := range t.buffer {
@ -507,11 +509,10 @@ func (t *TextView) reindexBuffer(width int) {
var (
colorTagIndices [][]int
colorTags [][]string
escapeIndices [][]int
)
if t.dynamicColors {
colorTagIndices = colorPattern.FindAllStringIndex(str, -1)
colorTags = colorPattern.FindAllStringSubmatch(str, -1)
str = colorPattern.ReplaceAllString(str, "")
colorTagIndices, colorTags, escapeIndices, str, _ = decomposeString(str)
}
// Find all regions in this line. Then remove them.
@ -523,13 +524,11 @@ func (t *TextView) reindexBuffer(width int) {
regionIndices = regionPattern.FindAllStringIndex(str, -1)
regions = regionPattern.FindAllStringSubmatch(str, -1)
str = regionPattern.ReplaceAllString(str, "")
}
// Find all replace tags in this line. Then replace them.
var escapeIndices [][]int
if t.dynamicColors || t.regions {
escapeIndices = escapePattern.FindAllStringIndex(str, -1)
str = escapePattern.ReplaceAllString(str, "[$1$2]")
if !t.dynamicColors {
// We haven't detected escape tags yet. Do it now.
escapeIndices = escapePattern.FindAllStringIndex(str, -1)
str = escapePattern.ReplaceAllString(str, "[$1$2]")
}
}
// Split the line if required.
@ -562,10 +561,11 @@ func (t *TextView) reindexBuffer(width int) {
var originalPos, colorPos, regionPos, escapePos int
for _, splitLine := range splitLines {
line := &textViewIndex{
Line: bufferIndex,
Pos: originalPos,
Color: color,
Region: regionID,
Line: bufferIndex,
Pos: originalPos,
Style: style,
OverwriteAttr: overwriteAttr,
Region: regionID,
}
// Shift original position with tags.
@ -574,7 +574,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]
color = tcell.GetColor(colorTags[colorPos][1])
style, overwriteAttr = styleFromTag(style, overwriteAttr, colorTags[colorPos])
colorPos++
} else if regionPos < len(regionIndices) && regionIndices[regionPos][0] <= originalPos+lineLength {
// Process region tags.
@ -721,17 +721,18 @@ 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]
color := index.Color
style := index.Style
overwriteAttr := index.OverwriteAttr
regionID := index.Region
// Get color tags.
var (
colorTagIndices [][]int
colorTags [][]string
escapeIndices [][]int
)
if t.dynamicColors {
colorTagIndices = colorPattern.FindAllStringIndex(text, -1)
colorTags = colorPattern.FindAllStringSubmatch(text, -1)
colorTagIndices, colorTags, escapeIndices, _, _ = decomposeString(text)
}
// Get regions.
@ -742,12 +743,9 @@ func (t *TextView) Draw(screen tcell.Screen) {
if t.regions {
regionIndices = regionPattern.FindAllStringIndex(text, -1)
regions = regionPattern.FindAllStringSubmatch(text, -1)
}
// Get escape tags.
var escapeIndices [][]int
if t.dynamicColors || t.regions {
escapeIndices = escapePattern.FindAllStringIndex(text, -1)
if !t.dynamicColors {
escapeIndices = escapePattern.FindAllStringIndex(text, -1)
}
}
// Calculate the position of the line.
@ -770,7 +768,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 {
color = tcell.GetColor(colorTags[currentTag][1])
style, overwriteAttr = styleFromTag(style, overwriteAttr, colorTags[currentTag])
currentTag++
}
continue
@ -812,16 +810,27 @@ func (t *TextView) Draw(screen tcell.Screen) {
}
// Do we highlight this character?
style := tcell.StyleDefault.Background(t.backgroundColor).Foreground(color)
finalStyle := style
if len(regionID) > 0 {
if _, ok := t.highlights[regionID]; ok {
style = tcell.StyleDefault.Background(color).Foreground(t.backgroundColor)
fg, bg, _ := finalStyle.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}
_, _, li := c.Hcl()
if li < .5 {
bg = tcell.ColorWhite
} else {
bg = tcell.ColorBlack
}
}
finalStyle = 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, style)
screen.SetContent(x+posX+offset, y+line-t.lineOffset, ch, nil, finalStyle)
}
// Advance.

176
util.go
View File

@ -104,13 +104,20 @@ var joints = map[string]rune{
// Common regular expressions.
var (
colorPattern = regexp.MustCompile(`\[([a-zA-Z]+|#[0-9a-zA-Z]{6})\]`)
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_,;: \-\.]*"|[a-zA-Z]+|#[0-9a-zA-Z]{6})\[(\[*)\]`)
escapePattern = regexp.MustCompile(`\[([a-zA-Z0-9_,;: \-\."#]+)\[(\[*)\]`)
boundaryPattern = regexp.MustCompile("([[:punct:]]\\s*|\\s+)")
spacePattern = regexp.MustCompile(`\s+`)
)
// Positions of substrings in regular expressions.
const (
colorForegroundPos = 1
colorBackgroundPos = 3
colorFlagPos = 5
)
// Predefined InputField acceptance functions.
var (
// InputFieldInteger accepts integers.
@ -150,40 +157,148 @@ 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.
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))
}
}
// Flags.
specified := tagSubstrings[colorFlagPos-1] != ""
if specified {
overwriteAttr = true
style = style.Normal()
for _, flag := range tagSubstrings[colorFlagPos] {
switch flag {
case 'l':
style = style.Blink(true)
case 'b':
style = style.Bold(true)
case 'd':
style = style.Dim(true)
case 'r':
style = style.Reverse(true)
case 'u':
style = style.Underline(true)
}
}
}
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
}
// decomposeString returns information about a string which may contain color
// tags. It returns the indices of the color tags (as returned by
// re.FindAllStringIndex()), the color tags themselves (as returned by
// re.FindAllStringSubmatch()), the indices of an escaped tags, the string
// stripped by any color tags and escaped, and the screen width of the stripped
// string.
func decomposeString(text string) (colorIndices [][]int, colors [][]string, escapeIndices [][]int, stripped string, width int) {
// Get positions of color and escape tags.
colorIndices = colorPattern.FindAllStringIndex(text, -1)
colors = colorPattern.FindAllStringSubmatch(text, -1)
escapeIndices = escapePattern.FindAllStringIndex(text, -1)
// Because the color pattern detects empty tags, we need to filter them out.
for i := len(colorIndices) - 1; i >= 0; i-- {
if colorIndices[i][1]-colorIndices[i][0] == 2 {
colorIndices = append(colorIndices[:i], colorIndices[i+1:]...)
colors = append(colors[:i], colors[i+1:]...)
}
}
// Remove the color tags from the original string.
var from int
buf := make([]byte, 0, len(text))
for _, indices := range colorIndices {
buf = append(buf, []byte(text[from:indices[0]])...)
from = indices[1]
}
buf = append(buf, text[from:]...)
// Escape string.
stripped = string(escapePattern.ReplaceAll(buf, []byte("[$1$2]")))
// Get the width of the stripped string.
width = runewidth.StringWidth(stripped)
return
}
// Print prints text onto the screen into the given box at (x,y,maxWidth,1),
// not exceeding that box. "align" is one of AlignLeft, AlignCenter, or
// AlignRight. The screen's background color will not be changed.
//
// You can change the text color mid-text by inserting a color tag. See the
// package description for details.
// You can change the colors and text styles mid-text by inserting a color tag.
// See the package description for details.
//
// 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)
}
// 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) {
if maxWidth < 0 {
return 0, 0
}
// Get positions of color and escape tags. Remove them from original string.
colorIndices := colorPattern.FindAllStringIndex(text, -1)
colors := colorPattern.FindAllStringSubmatch(text, -1)
escapeIndices := escapePattern.FindAllStringIndex(text, -1)
strippedText := escapePattern.ReplaceAllString(colorPattern.ReplaceAllString(text, ""), "[$1$2]")
// Decompose the text.
colorIndices, colors, escapeIndices, strippedText, _ := decomposeString(text)
// We deal with runes, not with bytes.
runes := []rune(strippedText)
// This helper function takes positions for a substring of "runes" and a start
// color and returns the substring with the original tags and the new start
// color.
substring := func(from, to int, color tcell.Color) (string, tcell.Color) {
// 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
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 {
color = tcell.GetColor(colors[colorPos][1])
style, overwriteAttr = styleFromTag(style, overwriteAttr, colors[colorPos])
}
colorPos++
}
@ -203,13 +318,13 @@ func Print(screen tcell.Screen, text string, x, y, maxWidth, align int, color tc
if runePos == from {
startPos = pos
} else if runePos >= to {
return text[startPos:pos], color
return text[startPos:pos], style, overwriteAttr
}
runePos++
}
return text[startPos:], color
return text[startPos:], style, overwriteAttr
}
// We want to reduce everything to AlignLeft.
@ -224,17 +339,17 @@ func Print(screen tcell.Screen, text string, x, y, maxWidth, align int, color tc
width += w
start = index
}
text, color = substring(start, len(runes), color)
return Print(screen, text, x+maxWidth-width, y, width, AlignLeft, color)
text, style, overwriteAttr = substring(start, len(runes), style, overwriteAttr)
return printWithStyle(screen, text, x+maxWidth-width, y, width, AlignLeft, style, overwriteAttr)
} else if align == AlignCenter {
width := runewidth.StringWidth(strippedText)
if width == maxWidth {
// Use the exact space.
return Print(screen, text, x, y, maxWidth, AlignLeft, color)
return printWithStyle(screen, text, x, y, maxWidth, AlignLeft, style, overwriteAttr)
} else if width < maxWidth {
// We have more space than we need.
half := (maxWidth - width) / 2
return Print(screen, text, x+half, y, maxWidth-half, AlignLeft, color)
return printWithStyle(screen, text, x+half, y, maxWidth-half, AlignLeft, style, overwriteAttr)
} else {
// Chop off runes until we have a perfect fit.
var choppedLeft, choppedRight, leftIndex, rightIndex int
@ -250,8 +365,8 @@ func Print(screen tcell.Screen, text string, x, y, maxWidth, align int, color tc
rightIndex--
}
}
text, color = substring(leftIndex, rightIndex, color)
return Print(screen, text, x, y, maxWidth, AlignLeft, color)
text, style, overwriteAttr = substring(leftIndex, rightIndex, style, overwriteAttr)
return printWithStyle(screen, text, x, y, maxWidth, AlignLeft, style, overwriteAttr)
}
}
@ -263,7 +378,7 @@ func Print(screen tcell.Screen, text string, x, y, maxWidth, align int, color tc
// Handle color tags.
if colorPos < len(colorIndices) && pos >= colorIndices[colorPos][0] && pos < colorIndices[colorPos][1] {
if pos == colorIndices[colorPos][1]-1 {
color = tcell.GetColor(colors[colorPos][1])
style, overwriteAttr = styleFromTag(style, overwriteAttr, colors[colorPos])
colorPos++
}
continue
@ -286,11 +401,11 @@ func Print(screen tcell.Screen, text string, x, y, maxWidth, align int, color tc
finalX := x + drawnWidth
// Print the rune.
_, _, style, _ := screen.GetContent(finalX, y)
style = style.Foreground(color)
_, _, finalStyle, _ := screen.GetContent(finalX, y)
finalStyle = overlayStyle(finalStyle, style, overwriteAttr)
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, style)
screen.SetContent(finalX+offset, y, ch, nil, finalStyle)
}
drawn++
@ -308,7 +423,8 @@ func PrintSimple(screen tcell.Screen, text string, x, y int) {
// StringWidth returns the width of the given string needed to print it on
// screen. The text may contain color tags which are not counted.
func StringWidth(text string) int {
return runewidth.StringWidth(escapePattern.ReplaceAllString(colorPattern.ReplaceAllString(text, ""), "[$1$2]"))
_, _, _, _, width := decomposeString(text)
return width
}
// WordWrap splits a text such that each resulting line does not exceed the
@ -319,13 +435,7 @@ func StringWidth(text string) int {
//
// Text is always split at newline characters ('\n').
func WordWrap(text string, width int) (lines []string) {
// Strip color tags.
strippedText := escapePattern.ReplaceAllString(colorPattern.ReplaceAllString(text, ""), "[$1$2]")
// Keep track of color tags and escape patterns so we can restore the original
// indices.
colorTagIndices := colorPattern.FindAllStringIndex(text, -1)
escapeIndices := escapePattern.FindAllStringIndex(text, -1)
colorTagIndices, _, escapeIndices, strippedText, _ := decomposeString(text)
// Find candidate breakpoints.
breakPoints := boundaryPattern.FindAllStringIndex(strippedText, -1)