|
|
|
@ -101,9 +101,9 @@ func init() {
|
|
|
|
|
// 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]
|
|
|
|
|
func styleFromTag(fgColor, bgColor, attributes string, tagSubstrings [][]byte) (newFgColor, newBgColor, newAttributes string) {
|
|
|
|
|
if len(tagSubstrings[colorForegroundPos]) > 0 {
|
|
|
|
|
color := string(tagSubstrings[colorForegroundPos])
|
|
|
|
|
if color == "-" {
|
|
|
|
|
fgColor = "-"
|
|
|
|
|
} else if color != "" {
|
|
|
|
@ -111,8 +111,8 @@ func styleFromTag(fgColor, bgColor, attributes string, tagSubstrings []string) (
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if tagSubstrings[colorBackgroundPos-1] != "" {
|
|
|
|
|
color := tagSubstrings[colorBackgroundPos]
|
|
|
|
|
if len(tagSubstrings[colorBackgroundPos-1]) > 0 {
|
|
|
|
|
color := string(tagSubstrings[colorBackgroundPos])
|
|
|
|
|
if color == "-" {
|
|
|
|
|
bgColor = "-"
|
|
|
|
|
} else if color != "" {
|
|
|
|
@ -120,8 +120,8 @@ func styleFromTag(fgColor, bgColor, attributes string, tagSubstrings []string) (
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if tagSubstrings[colorFlagPos-1] != "" {
|
|
|
|
|
flags := tagSubstrings[colorFlagPos]
|
|
|
|
|
if len(tagSubstrings[colorFlagPos-1]) > 0 {
|
|
|
|
|
flags := string(tagSubstrings[colorFlagPos])
|
|
|
|
|
if flags == "-" {
|
|
|
|
|
attributes = "-"
|
|
|
|
|
} else if flags != "" {
|
|
|
|
@ -200,7 +200,7 @@ func SetAttributes(style tcell.Style, attrs tcell.AttrMask) tcell.Style {
|
|
|
|
|
Underline(attrs&tcell.AttrUnderline != 0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// decomposeString returns information about a string which may contain color
|
|
|
|
|
// decomposeText returns information about a string which may contain color
|
|
|
|
|
// tags or region tags, depending on which ones are requested to be found. It
|
|
|
|
|
// returns the indices of the color tags (as returned by
|
|
|
|
|
// re.FindAllStringIndex()), the color tags themselves (as returned by
|
|
|
|
@ -208,22 +208,22 @@ func SetAttributes(style tcell.Style, attrs tcell.AttrMask) tcell.Style {
|
|
|
|
|
// themselves, the indices of an escaped tags (only if at least color tags or
|
|
|
|
|
// region tags are requested), the string stripped by any tags and escaped, and
|
|
|
|
|
// the screen width of the stripped string.
|
|
|
|
|
func decomposeString(text string, findColors, findRegions bool) (colorIndices [][]int, colors [][]string, regionIndices [][]int, regions [][]string, escapeIndices [][]int, stripped string, width int) {
|
|
|
|
|
func decomposeText(text []byte, findColors, findRegions bool) (colorIndices [][]int, colors [][][]byte, regionIndices [][]int, regions [][][]byte, escapeIndices [][]int, stripped []byte, width int) {
|
|
|
|
|
// Shortcut for the trivial case.
|
|
|
|
|
if !findColors && !findRegions {
|
|
|
|
|
return nil, nil, nil, nil, nil, text, stringWidth(text)
|
|
|
|
|
return nil, nil, nil, nil, nil, text, runewidth.StringWidth(string(text))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get positions of any tags.
|
|
|
|
|
if findColors {
|
|
|
|
|
colorIndices = colorPattern.FindAllStringIndex(text, -1)
|
|
|
|
|
colors = colorPattern.FindAllStringSubmatch(text, -1)
|
|
|
|
|
colorIndices = colorPattern.FindAllIndex(text, -1)
|
|
|
|
|
colors = colorPattern.FindAllSubmatch(text, -1)
|
|
|
|
|
}
|
|
|
|
|
if findRegions {
|
|
|
|
|
regionIndices = regionPattern.FindAllStringIndex(text, -1)
|
|
|
|
|
regions = regionPattern.FindAllStringSubmatch(text, -1)
|
|
|
|
|
regionIndices = regionPattern.FindAllIndex(text, -1)
|
|
|
|
|
regions = regionPattern.FindAllSubmatch(text, -1)
|
|
|
|
|
}
|
|
|
|
|
escapeIndices = escapePattern.FindAllStringIndex(text, -1)
|
|
|
|
|
escapeIndices = escapePattern.FindAllIndex(text, -1)
|
|
|
|
|
|
|
|
|
|
// Because the color pattern detects empty tags, we need to filter them out.
|
|
|
|
|
for i := len(colorIndices) - 1; i >= 0; i-- {
|
|
|
|
@ -259,10 +259,10 @@ func decomposeString(text string, findColors, findRegions bool) (colorIndices []
|
|
|
|
|
buf = append(buf, text[from:]...)
|
|
|
|
|
|
|
|
|
|
// Escape string.
|
|
|
|
|
stripped = string(escapePattern.ReplaceAll(buf, []byte("[$1$2]")))
|
|
|
|
|
stripped = escapePattern.ReplaceAll(buf, []byte("[$1$2]"))
|
|
|
|
|
|
|
|
|
|
// Get the width of the stripped string.
|
|
|
|
|
width = stringWidth(stripped)
|
|
|
|
|
width = runewidth.StringWidth(string(stripped))
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
@ -276,19 +276,19 @@ func decomposeString(text string, findColors, findRegions bool) (colorIndices []
|
|
|
|
|
//
|
|
|
|
|
// Returns the number of actual bytes of the text printed (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) {
|
|
|
|
|
func Print(screen tcell.Screen, text []byte, x, y, maxWidth, align int, color tcell.Color) (int, int) {
|
|
|
|
|
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.
|
|
|
|
|
func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, style tcell.Style) (int, int) {
|
|
|
|
|
func printWithStyle(screen tcell.Screen, text []byte, x, y, maxWidth, align int, style tcell.Style) (int, int) {
|
|
|
|
|
if maxWidth <= 0 || len(text) == 0 {
|
|
|
|
|
return 0, 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Decompose the text.
|
|
|
|
|
colorIndices, colors, _, _, escapeIndices, strippedText, strippedWidth := decomposeString(text, true, false)
|
|
|
|
|
colorIndices, colors, _, _, escapeIndices, strippedText, strippedWidth := decomposeText(text, true, false)
|
|
|
|
|
|
|
|
|
|
// We want to reduce all alignments to AlignLeft.
|
|
|
|
|
if align == AlignRight {
|
|
|
|
@ -302,7 +302,7 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int,
|
|
|
|
|
foregroundColor, backgroundColor, attributes string
|
|
|
|
|
)
|
|
|
|
|
_, originalBackground, _ := style.Decompose()
|
|
|
|
|
iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
|
|
|
|
iterateString(string(strippedText), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
|
|
|
|
// Update color/escape tag offset and style.
|
|
|
|
|
if colorPos < len(colorIndices) && textPos+tagOffset >= colorIndices[colorPos][0] && textPos+tagOffset < colorIndices[colorPos][1] {
|
|
|
|
|
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
|
|
|
|
@ -319,7 +319,7 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int,
|
|
|
|
|
if escapePos > 0 && textPos+tagOffset-1 >= escapeIndices[escapePos-1][0] && textPos+tagOffset-1 < escapeIndices[escapePos-1][1] {
|
|
|
|
|
// Unescape open escape sequences.
|
|
|
|
|
escapeCharPos := escapeIndices[escapePos-1][1] - 2
|
|
|
|
|
text = text[:escapeCharPos] + text[escapeCharPos+1:]
|
|
|
|
|
text = append(text[:escapeCharPos], text[escapeCharPos+1:]...)
|
|
|
|
|
}
|
|
|
|
|
// Print and return.
|
|
|
|
|
bytes, width = printWithStyle(screen, text[textPos+tagOffset:], x, y, maxWidth, AlignLeft, style)
|
|
|
|
@ -343,14 +343,14 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int,
|
|
|
|
|
for rightIndex-1 > leftIndex && strippedWidth-choppedLeft-choppedRight > maxWidth {
|
|
|
|
|
if choppedLeft < choppedRight {
|
|
|
|
|
// Iterate on the left by one character.
|
|
|
|
|
iterateString(strippedText[leftIndex:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
|
|
|
|
iterateString(string(strippedText[leftIndex:]), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
|
|
|
|
choppedLeft += screenWidth
|
|
|
|
|
leftIndex += textWidth
|
|
|
|
|
return true
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
// Iterate on the right by one character.
|
|
|
|
|
iterateStringReverse(strippedText[leftIndex:rightIndex], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
|
|
|
|
iterateStringReverse(string(strippedText[leftIndex:rightIndex]), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
|
|
|
|
choppedRight += screenWidth
|
|
|
|
|
rightIndex -= textWidth
|
|
|
|
|
return true
|
|
|
|
@ -371,7 +371,7 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int,
|
|
|
|
|
if escapePos > 0 && leftIndex+tagOffset-1 >= escapeIndices[escapePos-1][0] && leftIndex+tagOffset-1 < escapeIndices[escapePos-1][1] {
|
|
|
|
|
// Unescape open escape sequences.
|
|
|
|
|
escapeCharPos := escapeIndices[escapePos-1][1] - 2
|
|
|
|
|
text = text[:escapeCharPos] + text[escapeCharPos+1:]
|
|
|
|
|
text = append(text[:escapeCharPos], text[escapeCharPos+1:]...)
|
|
|
|
|
}
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
@ -399,7 +399,7 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int,
|
|
|
|
|
drawn, drawnWidth, colorPos, escapePos, tagOffset int
|
|
|
|
|
foregroundColor, backgroundColor, attributes string
|
|
|
|
|
)
|
|
|
|
|
iterateString(strippedText, func(main rune, comb []rune, textPos, length, screenPos, screenWidth int) bool {
|
|
|
|
|
iterateString(string(strippedText), func(main rune, comb []rune, textPos, length, screenPos, screenWidth int) bool {
|
|
|
|
|
// Only continue if there is still space.
|
|
|
|
|
if drawnWidth+screenWidth > maxWidth {
|
|
|
|
|
return true
|
|
|
|
@ -445,33 +445,21 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PrintSimple prints white text to the screen at the given position.
|
|
|
|
|
func PrintSimple(screen tcell.Screen, text string, x, y int) {
|
|
|
|
|
func PrintSimple(screen tcell.Screen, text []byte, x, y int) {
|
|
|
|
|
Print(screen, text, x, y, math.MaxInt32, AlignLeft, Styles.PrimaryTextColor)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TaggedStringWidth returns the width of the given string needed to print it on
|
|
|
|
|
// TaggedTextWidth returns the width of the given string needed to print it on
|
|
|
|
|
// screen. The text may contain color tags which are not counted.
|
|
|
|
|
func TaggedStringWidth(text string) int {
|
|
|
|
|
_, _, _, _, _, _, width := decomposeString(text, true, false)
|
|
|
|
|
func TaggedTextWidth(text []byte) int {
|
|
|
|
|
_, _, _, _, _, _, width := decomposeText(text, true, false)
|
|
|
|
|
return width
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// stringWidth returns the number of horizontal cells needed to print the given
|
|
|
|
|
// text. It splits the text into its grapheme clusters, calculates each
|
|
|
|
|
// cluster's width, and adds them up to a total.
|
|
|
|
|
func stringWidth(text string) (width int) {
|
|
|
|
|
g := uniseg.NewGraphemes(text)
|
|
|
|
|
for g.Next() {
|
|
|
|
|
var chWidth int
|
|
|
|
|
for _, r := range g.Runes() {
|
|
|
|
|
chWidth = runewidth.RuneWidth(r)
|
|
|
|
|
if chWidth > 0 {
|
|
|
|
|
break // Our best guess at this point is to use the width of the first non-zero-width rune.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
width += chWidth
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
// TaggedStringWidth returns the width of the given string needed to print it on
|
|
|
|
|
// screen. The text may contain color tags which are not counted.
|
|
|
|
|
func TaggedStringWidth(text string) int {
|
|
|
|
|
return TaggedTextWidth([]byte(text))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// WordWrap splits a text such that each resulting line does not exceed the
|
|
|
|
@ -481,11 +469,11 @@ func stringWidth(text string) (width int) {
|
|
|
|
|
// This function considers color tags to have no width.
|
|
|
|
|
//
|
|
|
|
|
// Text is always split at newline characters ('\n').
|
|
|
|
|
func WordWrap(text string, width int) (lines []string) {
|
|
|
|
|
colorTagIndices, _, _, _, escapeIndices, strippedText, _ := decomposeString(text, true, false)
|
|
|
|
|
func WordWrap(text []byte, width int) (lines [][]byte) {
|
|
|
|
|
colorTagIndices, _, _, _, escapeIndices, strippedText, _ := decomposeText(text, true, false)
|
|
|
|
|
|
|
|
|
|
// Find candidate breakpoints.
|
|
|
|
|
breakpoints := boundaryPattern.FindAllStringSubmatchIndex(strippedText, -1)
|
|
|
|
|
breakpoints := boundaryPattern.FindAllSubmatchIndex(strippedText, -1)
|
|
|
|
|
// Results in one entry for each candidate. Each entry is an array a of
|
|
|
|
|
// indices into strippedText where a[6] < 0 for newline/punctuation matches
|
|
|
|
|
// and a[4] < 0 for whitespace matches.
|
|
|
|
@ -497,17 +485,17 @@ func WordWrap(text string, width int) (lines []string) {
|
|
|
|
|
lineWidth, overflow int
|
|
|
|
|
forceBreak bool
|
|
|
|
|
)
|
|
|
|
|
unescape := func(substr string, startIndex int) string {
|
|
|
|
|
unescape := func(substr []byte, startIndex int) []byte {
|
|
|
|
|
// A helper function to unescape escaped tags.
|
|
|
|
|
for index := escapePos; index >= 0; index-- {
|
|
|
|
|
if index < len(escapeIndices) && startIndex > escapeIndices[index][0] && startIndex < escapeIndices[index][1]-1 {
|
|
|
|
|
pos := escapeIndices[index][1] - 2 - startIndex
|
|
|
|
|
return substr[:pos] + substr[pos+1:]
|
|
|
|
|
return append(substr[:pos], substr[pos+1:]...)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return substr
|
|
|
|
|
}
|
|
|
|
|
iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
|
|
|
|
iterateString(string(strippedText), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
|
|
|
|
// Handle tags.
|
|
|
|
|
for {
|
|
|
|
|
if colorPos < len(colorTagIndices) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] {
|
|
|
|
@ -603,7 +591,7 @@ func iterateString(text string, callback func(main rune, comb []rune, textPos, t
|
|
|
|
|
for gr.Next() {
|
|
|
|
|
r := gr.Runes()
|
|
|
|
|
from, to := gr.Positions()
|
|
|
|
|
width := stringWidth(gr.Str())
|
|
|
|
|
width := runewidth.StringWidth(gr.Str())
|
|
|
|
|
var comb []rune
|
|
|
|
|
if len(r) > 1 {
|
|
|
|
|
comb = r[1:]
|
|
|
|
@ -679,7 +667,15 @@ const (
|
|
|
|
|
ScrollBarAlways
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// RenderScrollBar renders a scroll bar character at the specified position.
|
|
|
|
|
// Scroll bar render text (must be one cell wide)
|
|
|
|
|
var (
|
|
|
|
|
ScrollBarArea = []byte("[-:-:-]░")
|
|
|
|
|
ScrollBarAreaFocused = []byte("[-:-:-]▒")
|
|
|
|
|
ScrollBarHandle = []byte("[-:-:-]▓")
|
|
|
|
|
ScrollBarHandleFocused = []byte("[::r] [-:-:-]")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// RenderScrollBar renders a scroll bar at the specified position.
|
|
|
|
|
func RenderScrollBar(screen tcell.Screen, visibility ScrollBarVisibility, x int, y int, height int, items int, cursor int, printed int, focused bool, color tcell.Color) {
|
|
|
|
|
if visibility == ScrollBarNever || (visibility == ScrollBarAuto && items <= height) {
|
|
|
|
|
return
|
|
|
|
@ -698,20 +694,20 @@ func RenderScrollBar(screen tcell.Screen, visibility ScrollBarVisibility, x int,
|
|
|
|
|
// Calculate handle position.
|
|
|
|
|
handlePosition := int(float64(height-1) * (float64(cursor) / float64(items-1)))
|
|
|
|
|
|
|
|
|
|
// Print character.
|
|
|
|
|
var scrollBar string
|
|
|
|
|
// Print scroll bar.
|
|
|
|
|
var text []byte
|
|
|
|
|
if printed == handlePosition {
|
|
|
|
|
if focused {
|
|
|
|
|
scrollBar = "[::r] [-:-:-]"
|
|
|
|
|
text = ScrollBarHandleFocused
|
|
|
|
|
} else {
|
|
|
|
|
scrollBar = "[-:-:-]▓"
|
|
|
|
|
text = ScrollBarHandle
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if focused {
|
|
|
|
|
scrollBar = "[-:-:-]▒"
|
|
|
|
|
text = ScrollBarAreaFocused
|
|
|
|
|
} else {
|
|
|
|
|
scrollBar = "[-:-:-]░"
|
|
|
|
|
text = ScrollBarArea
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Print(screen, scrollBar, x, y, 1, AlignLeft, color)
|
|
|
|
|
Print(screen, text, x, y, 1, AlignLeft, color)
|
|
|
|
|
}
|
|
|
|
|