mirror of https://github.com/rivo/tview.git
679 lines
24 KiB
Go
679 lines
24 KiB
Go
package tview
|
|
|
|
import (
|
|
"math"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
|
|
"github.com/gdamore/tcell/v2"
|
|
runewidth "github.com/mattn/go-runewidth"
|
|
"github.com/rivo/uniseg"
|
|
)
|
|
|
|
// Text alignment within a box.
|
|
const (
|
|
AlignLeft = iota
|
|
AlignCenter
|
|
AlignRight
|
|
)
|
|
|
|
// Common regular expressions.
|
|
var (
|
|
colorPattern = regexp.MustCompile(`\[([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([lbidrus]+|\-)?)?)?\]`)
|
|
regionPattern = regexp.MustCompile(`\["([a-zA-Z0-9_,;: \-\.]*)"\]`)
|
|
escapePattern = regexp.MustCompile(`\[([a-zA-Z0-9_,;: \-\."#]+)\[(\[*)\]`)
|
|
nonEscapePattern = regexp.MustCompile(`(\[[a-zA-Z0-9_,;: \-\."#]+\[*)\]`)
|
|
boundaryPattern = regexp.MustCompile(`(([,\.\-:;!\?&#+]|\n)[ \t\f\r]*|([ \t\f\r]+))`)
|
|
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.
|
|
InputFieldInteger func(text string, ch rune) bool
|
|
|
|
// InputFieldFloat accepts floating-point numbers.
|
|
InputFieldFloat func(text string, ch rune) bool
|
|
|
|
// InputFieldMaxLength returns an input field accept handler which accepts
|
|
// input strings up to a given length. Use it like this:
|
|
//
|
|
// inputField.SetAcceptanceFunc(InputFieldMaxLength(10)) // Accept up to 10 characters.
|
|
InputFieldMaxLength func(maxLength int) func(text string, ch rune) bool
|
|
)
|
|
|
|
// Package initialization.
|
|
func init() {
|
|
// Initialize the predefined input field handlers.
|
|
InputFieldInteger = func(text string, ch rune) bool {
|
|
if text == "-" {
|
|
return true
|
|
}
|
|
_, err := strconv.Atoi(text)
|
|
return err == nil
|
|
}
|
|
InputFieldFloat = func(text string, ch rune) bool {
|
|
if text == "-" || text == "." || text == "-." {
|
|
return true
|
|
}
|
|
_, err := strconv.ParseFloat(text, 64)
|
|
return err == nil
|
|
}
|
|
InputFieldMaxLength = func(maxLength int) func(text string, ch rune) bool {
|
|
return func(text string, ch rune) bool {
|
|
return len([]rune(text)) <= maxLength
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 == "-" {
|
|
fgColor = "-"
|
|
} else if color != "" {
|
|
fgColor = color
|
|
}
|
|
}
|
|
|
|
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 calculates a new style based on "style" and applying tag-based
|
|
// colors/attributes to it (see also styleFromTag()).
|
|
func overlayStyle(style tcell.Style, fgColor, bgColor, attributes string) tcell.Style {
|
|
_, _, defAttr := style.Decompose()
|
|
|
|
if fgColor != "" && fgColor != "-" {
|
|
style = style.Foreground(tcell.GetColor(fgColor))
|
|
}
|
|
|
|
if bgColor != "" && bgColor != "-" {
|
|
style = style.Background(tcell.GetColor(bgColor))
|
|
}
|
|
|
|
if attributes == "-" {
|
|
style = style.Bold(defAttr&tcell.AttrBold > 0).
|
|
Italic(defAttr&tcell.AttrItalic > 0).
|
|
Blink(defAttr&tcell.AttrBlink > 0).
|
|
Reverse(defAttr&tcell.AttrReverse > 0).
|
|
Underline(defAttr&tcell.AttrUnderline > 0).
|
|
Dim(defAttr&tcell.AttrDim > 0)
|
|
} else if attributes != "" {
|
|
style = style.Normal()
|
|
for _, flag := range attributes {
|
|
switch flag {
|
|
case 'l':
|
|
style = style.Blink(true)
|
|
case 'b':
|
|
style = style.Bold(true)
|
|
case 'i':
|
|
style = style.Italic(true)
|
|
case 'd':
|
|
style = style.Dim(true)
|
|
case 'r':
|
|
style = style.Reverse(true)
|
|
case 'u':
|
|
style = style.Underline(true)
|
|
case 's':
|
|
style = style.StrikeThrough(true)
|
|
}
|
|
}
|
|
}
|
|
|
|
return style
|
|
}
|
|
|
|
// decomposeString 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
|
|
// re.FindAllStringSubmatch()), the indices of region tags and the region tags
|
|
// 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) {
|
|
// Shortcut for the trivial case.
|
|
if !findColors && !findRegions {
|
|
return nil, nil, nil, nil, nil, text, stringWidth(text)
|
|
}
|
|
|
|
// Get positions of any tags.
|
|
if findColors {
|
|
colorIndices = colorPattern.FindAllStringIndex(text, -1)
|
|
colors = colorPattern.FindAllStringSubmatch(text, -1)
|
|
}
|
|
if findRegions {
|
|
regionIndices = regionPattern.FindAllStringIndex(text, -1)
|
|
regions = regionPattern.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:]...)
|
|
}
|
|
}
|
|
|
|
// Make a (sorted) list of all tags.
|
|
allIndices := make([][3]int, 0, len(colorIndices)+len(regionIndices)+len(escapeIndices))
|
|
for indexType, index := range [][][]int{colorIndices, regionIndices, escapeIndices} {
|
|
for _, tag := range index {
|
|
allIndices = append(allIndices, [3]int{tag[0], tag[1], indexType})
|
|
}
|
|
}
|
|
sort.Slice(allIndices, func(i int, j int) bool {
|
|
return allIndices[i][0] < allIndices[j][0]
|
|
})
|
|
|
|
// Remove the tags from the original string.
|
|
var from int
|
|
buf := make([]byte, 0, len(text))
|
|
for _, indices := range allIndices {
|
|
if indices[2] == 2 { // Escape sequences are not simply removed.
|
|
buf = append(buf, []byte(text[from:indices[1]-2])...)
|
|
buf = append(buf, ']')
|
|
from = indices[1]
|
|
} else {
|
|
buf = append(buf, []byte(text[from:indices[0]])...)
|
|
from = indices[1]
|
|
}
|
|
}
|
|
buf = append(buf, text[from:]...)
|
|
stripped = string(buf)
|
|
|
|
// Get the width of the stripped string.
|
|
width = 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 colors and text styles mid-text by inserting a color tag.
|
|
// See the package description for details.
|
|
//
|
|
// 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) {
|
|
bytes, width, _, _ := printWithStyle(screen, text, x, y, 0, maxWidth, align, tcell.StyleDefault.Foreground(color), true)
|
|
return bytes, width
|
|
}
|
|
|
|
// printWithStyle works like Print() but it takes a style instead of just a
|
|
// foreground color. The skipWidth parameter specifies the number of cells
|
|
// skipped at the beginning of the text. It also returns the start and end index
|
|
// (exclusively) of the text actually printed. If maintainBackground is "true",
|
|
// The existing screen background is not changed (i.e. the style's background
|
|
// color is ignored).
|
|
func printWithStyle(screen tcell.Screen, text string, x, y, skipWidth, maxWidth, align int, style tcell.Style, maintainBackground bool) (int, int, int, int) {
|
|
totalWidth, totalHeight := screen.Size()
|
|
if maxWidth <= 0 || len(text) == 0 || y < 0 || y >= totalHeight {
|
|
return 0, 0, 0, 0
|
|
}
|
|
|
|
// Decompose the text.
|
|
colorIndices, colors, _, _, escapeIndices, strippedText, strippedWidth := decomposeString(text, true, false)
|
|
|
|
// We want to reduce all alignments to AlignLeft.
|
|
if align == AlignRight {
|
|
if strippedWidth-skipWidth <= maxWidth {
|
|
// There's enough space for the entire text.
|
|
return printWithStyle(screen, text, x+maxWidth-strippedWidth+skipWidth, y, skipWidth, maxWidth, AlignLeft, style, maintainBackground)
|
|
}
|
|
// Trim characters off the beginning.
|
|
var (
|
|
bytes, width, colorPos, escapePos, tagOffset, from, to int
|
|
foregroundColor, backgroundColor, attributes string
|
|
)
|
|
originalStyle := style
|
|
iterateString(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])
|
|
style = overlayStyle(originalStyle, foregroundColor, backgroundColor, attributes)
|
|
tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0]
|
|
colorPos++
|
|
}
|
|
if escapePos < len(escapeIndices) && textPos+tagOffset >= escapeIndices[escapePos][0] && textPos+tagOffset < escapeIndices[escapePos][1] {
|
|
tagOffset++
|
|
escapePos++
|
|
}
|
|
if strippedWidth-screenPos <= maxWidth {
|
|
// We chopped off enough.
|
|
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:]
|
|
}
|
|
// Print and return.
|
|
bytes, width, from, to = printWithStyle(screen, text[textPos+tagOffset:], x, y, 0, maxWidth, AlignLeft, style, maintainBackground)
|
|
from += textPos + tagOffset
|
|
to += textPos + tagOffset
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
return bytes, width, from, to
|
|
} else if align == AlignCenter {
|
|
if strippedWidth-skipWidth == maxWidth {
|
|
// Use the exact space.
|
|
return printWithStyle(screen, text, x, y, skipWidth, maxWidth, AlignLeft, style, maintainBackground)
|
|
} else if strippedWidth-skipWidth < maxWidth {
|
|
// We have more space than we need.
|
|
half := (maxWidth - strippedWidth + skipWidth) / 2
|
|
return printWithStyle(screen, text, x+half, y, skipWidth, maxWidth-half, AlignLeft, style, maintainBackground)
|
|
} else {
|
|
// Chop off runes until we have a perfect fit.
|
|
var choppedLeft, choppedRight, leftIndex, rightIndex int
|
|
rightIndex = len(strippedText)
|
|
for rightIndex-1 > leftIndex && strippedWidth-skipWidth-choppedLeft-choppedRight > maxWidth {
|
|
if skipWidth > 0 || choppedLeft < choppedRight {
|
|
// Iterate on the left by one character.
|
|
iterateString(strippedText[leftIndex:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
|
if skipWidth > 0 {
|
|
skipWidth -= screenWidth
|
|
strippedWidth -= screenWidth
|
|
} else {
|
|
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 {
|
|
choppedRight += screenWidth
|
|
rightIndex -= textWidth
|
|
return true
|
|
})
|
|
}
|
|
}
|
|
|
|
// Add tag offsets and determine start style.
|
|
var (
|
|
colorPos, escapePos, tagOffset int
|
|
foregroundColor, backgroundColor, attributes string
|
|
)
|
|
originalStyle := style
|
|
for index := range strippedText {
|
|
// We only need the offset of the left index.
|
|
if index > leftIndex {
|
|
// We're done.
|
|
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:]
|
|
}
|
|
break
|
|
}
|
|
|
|
// Update color/escape tag offset.
|
|
if colorPos < len(colorIndices) && index+tagOffset >= colorIndices[colorPos][0] && index+tagOffset < colorIndices[colorPos][1] {
|
|
if index <= leftIndex {
|
|
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
|
|
style = overlayStyle(originalStyle, foregroundColor, backgroundColor, attributes)
|
|
}
|
|
tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0]
|
|
colorPos++
|
|
}
|
|
if escapePos < len(escapeIndices) && index+tagOffset >= escapeIndices[escapePos][0] && index+tagOffset < escapeIndices[escapePos][1] {
|
|
tagOffset++
|
|
escapePos++
|
|
}
|
|
}
|
|
bytes, width, from, to := printWithStyle(screen, text[leftIndex+tagOffset:], x, y, 0, maxWidth, AlignLeft, style, maintainBackground)
|
|
from += leftIndex + tagOffset
|
|
to += leftIndex + tagOffset
|
|
return bytes, width, from, to
|
|
}
|
|
}
|
|
|
|
// Draw text.
|
|
var (
|
|
drawn, drawnWidth, colorPos, escapePos, tagOffset, from, to int
|
|
foregroundColor, backgroundColor, attributes string
|
|
)
|
|
iterateString(strippedText, func(main rune, comb []rune, textPos, length, screenPos, screenWidth int) bool {
|
|
// Skip character if necessary.
|
|
if skipWidth > 0 {
|
|
skipWidth -= screenWidth
|
|
from = textPos + length
|
|
to = from
|
|
return false
|
|
}
|
|
|
|
// Only continue if there is still space.
|
|
if drawnWidth+screenWidth > maxWidth || x+drawnWidth >= totalWidth {
|
|
return true
|
|
}
|
|
|
|
// Handle color tags.
|
|
for colorPos < len(colorIndices) && textPos+tagOffset >= colorIndices[colorPos][0] && textPos+tagOffset < colorIndices[colorPos][1] {
|
|
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
|
|
tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0]
|
|
colorPos++
|
|
}
|
|
|
|
// Handle escape tags.
|
|
if escapePos < len(escapeIndices) && textPos+tagOffset >= escapeIndices[escapePos][0] && textPos+tagOffset < escapeIndices[escapePos][1] {
|
|
if textPos+tagOffset == escapeIndices[escapePos][1]-2 {
|
|
tagOffset++
|
|
escapePos++
|
|
}
|
|
}
|
|
|
|
// Memorize positions.
|
|
to = textPos + length
|
|
|
|
// Print the rune sequence.
|
|
finalX := x + drawnWidth
|
|
finalStyle := style
|
|
if maintainBackground {
|
|
_, _, existingStyle, _ := screen.GetContent(finalX, y)
|
|
_, background, _ := existingStyle.Decompose()
|
|
finalStyle = finalStyle.Background(background)
|
|
}
|
|
finalStyle = overlayStyle(finalStyle, foregroundColor, backgroundColor, attributes)
|
|
for offset := screenWidth - 1; offset >= 0; offset-- {
|
|
// To avoid undesired effects, we populate all cells.
|
|
if offset == 0 {
|
|
screen.SetContent(finalX+offset, y, main, comb, finalStyle)
|
|
} else {
|
|
screen.SetContent(finalX+offset, y, ' ', nil, finalStyle)
|
|
}
|
|
}
|
|
|
|
// Advance.
|
|
drawn += length
|
|
drawnWidth += screenWidth
|
|
|
|
return false
|
|
})
|
|
|
|
return drawn + tagOffset + len(escapeIndices), drawnWidth, from, to
|
|
}
|
|
|
|
// PrintSimple prints white text to the screen at the given position.
|
|
func PrintSimple(screen tcell.Screen, text string, 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
|
|
// screen. The text may contain color tags which are not counted.
|
|
func TaggedStringWidth(text string) int {
|
|
_, _, _, _, _, _, width := decomposeString(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) {
|
|
state := -1
|
|
for len(text) > 0 {
|
|
var (
|
|
chWidth int
|
|
cl string
|
|
)
|
|
cl, text, _, state = uniseg.FirstGraphemeClusterInString(text, state)
|
|
for _, r := range cl {
|
|
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
|
|
}
|
|
|
|
// WordWrap splits a text such that each resulting line does not exceed the
|
|
// given screen width. Possible split points are after any punctuation or
|
|
// whitespace. Whitespace after split points will be dropped.
|
|
//
|
|
// 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)
|
|
|
|
// Find candidate breakpoints.
|
|
breakpoints := boundaryPattern.FindAllStringSubmatchIndex(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.
|
|
|
|
// Process stripped text one character at a time.
|
|
var (
|
|
colorPos, escapePos, breakpointPos, tagOffset int
|
|
lastBreakpoint, lastContinuation, currentLineStart int
|
|
lineWidth, overflow int
|
|
forceBreak bool
|
|
)
|
|
unescape := func(substr string, startIndex int) string {
|
|
// 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 substr
|
|
}
|
|
iterateString(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] {
|
|
// Colour tags.
|
|
tagOffset += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0]
|
|
colorPos++
|
|
} else if escapePos < len(escapeIndices) && textPos+tagOffset == escapeIndices[escapePos][1]-2 {
|
|
// Escape tags.
|
|
tagOffset++
|
|
escapePos++
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Is this a breakpoint?
|
|
if breakpointPos < len(breakpoints) && textPos+tagOffset == breakpoints[breakpointPos][0] {
|
|
// Yes, it is. Set up breakpoint infos depending on its type.
|
|
lastBreakpoint = breakpoints[breakpointPos][0] + tagOffset
|
|
lastContinuation = breakpoints[breakpointPos][1] + tagOffset
|
|
overflow = 0
|
|
forceBreak = main == '\n'
|
|
if breakpoints[breakpointPos][6] < 0 && !forceBreak {
|
|
lastBreakpoint++ // Don't skip punctuation.
|
|
}
|
|
breakpointPos++
|
|
}
|
|
|
|
// Check if a break is warranted.
|
|
if forceBreak || lineWidth > 0 && lineWidth+screenWidth > width {
|
|
breakpoint := lastBreakpoint
|
|
continuation := lastContinuation
|
|
if forceBreak {
|
|
breakpoint = textPos + tagOffset
|
|
continuation = textPos + tagOffset + 1
|
|
lastBreakpoint = 0
|
|
overflow = 0
|
|
} else if lastBreakpoint <= currentLineStart {
|
|
breakpoint = textPos + tagOffset
|
|
continuation = textPos + tagOffset
|
|
overflow = 0
|
|
}
|
|
lines = append(lines, unescape(text[currentLineStart:breakpoint], currentLineStart))
|
|
currentLineStart, lineWidth, forceBreak = continuation, overflow, false
|
|
}
|
|
|
|
// Remember the characters since the last breakpoint.
|
|
if lastBreakpoint > 0 && lastContinuation <= textPos+tagOffset {
|
|
overflow += screenWidth
|
|
}
|
|
|
|
// Advance.
|
|
lineWidth += screenWidth
|
|
|
|
// But if we're still inside a breakpoint, skip next character (whitespace).
|
|
if textPos+tagOffset < currentLineStart {
|
|
lineWidth -= screenWidth
|
|
}
|
|
|
|
return false
|
|
})
|
|
|
|
// Flush the rest.
|
|
if currentLineStart < len(text) {
|
|
lines = append(lines, unescape(text[currentLineStart:], currentLineStart))
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Escape escapes the given text such that color and/or region tags are not
|
|
// recognized and substituted by the print functions of this package. For
|
|
// example, to include a tag-like string in a box title or in a TextView:
|
|
//
|
|
// box.SetTitle(tview.Escape("[squarebrackets]"))
|
|
// fmt.Fprint(textView, tview.Escape(`["quoted"]`))
|
|
func Escape(text string) string {
|
|
return nonEscapePattern.ReplaceAllString(text, "$1[]")
|
|
}
|
|
|
|
// iterateString iterates through the given string one printed character at a
|
|
// time. For each such character, the callback function is called with the
|
|
// Unicode code points of the character (the first rune and any combining runes
|
|
// which may be nil if there aren't any), the starting position (in bytes)
|
|
// within the original string, its length in bytes, the screen position of the
|
|
// character, and the screen width of it. The iteration stops if the callback
|
|
// returns true. This function returns true if the iteration was stopped before
|
|
// the last character.
|
|
func iterateString(text string, callback func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool) bool {
|
|
var screenPos, textPos int
|
|
|
|
state := -1
|
|
for len(text) > 0 {
|
|
var cluster string
|
|
cluster, text, _, state = uniseg.FirstGraphemeClusterInString(text, state)
|
|
|
|
var width int
|
|
runes := make([]rune, 0, len(cluster))
|
|
for _, r := range cluster {
|
|
runes = append(runes, r)
|
|
w := runewidth.RuneWidth(r)
|
|
if width == 0 && w > 0 {
|
|
width = w // Our best guess at this point is to use the width of the first non-zero-width rune.
|
|
}
|
|
}
|
|
|
|
var comb []rune
|
|
if len(runes) > 1 {
|
|
comb = runes[1:]
|
|
}
|
|
|
|
if callback(runes[0], comb, textPos, len(cluster), screenPos, width) {
|
|
return true
|
|
}
|
|
|
|
screenPos += width
|
|
textPos += len(cluster)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// iterateStringReverse iterates through the given string in reverse, starting
|
|
// from the end of the string, one printed character at a time. For each such
|
|
// character, the callback function is called with the Unicode code points of
|
|
// the character (the first rune and any combining runes which may be nil if
|
|
// there aren't any), the starting position (in bytes) within the original
|
|
// string, its length in bytes, the screen position of the character, and the
|
|
// screen width of it. The iteration stops if the callback returns true. This
|
|
// function returns true if the iteration was stopped before the last character.
|
|
func iterateStringReverse(text string, callback func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool) bool {
|
|
type cluster struct {
|
|
main rune
|
|
comb []rune
|
|
textPos, textWidth, screenPos, screenWidth int
|
|
}
|
|
|
|
// Create the grapheme clusters.
|
|
var clusters []cluster
|
|
iterateString(text, func(main rune, comb []rune, textPos int, textWidth int, screenPos int, screenWidth int) bool {
|
|
clusters = append(clusters, cluster{
|
|
main: main,
|
|
comb: comb,
|
|
textPos: textPos,
|
|
textWidth: textWidth,
|
|
screenPos: screenPos,
|
|
screenWidth: screenWidth,
|
|
})
|
|
return false
|
|
})
|
|
|
|
// Iterate in reverse.
|
|
for index := len(clusters) - 1; index >= 0; index-- {
|
|
if callback(
|
|
clusters[index].main,
|
|
clusters[index].comb,
|
|
clusters[index].textPos,
|
|
clusters[index].textWidth,
|
|
clusters[index].screenPos,
|
|
clusters[index].screenWidth,
|
|
) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// stripTags strips colour tags from the given string. (Region tags are not
|
|
// stripped.)
|
|
func stripTags(text string) string {
|
|
stripped := colorPattern.ReplaceAllStringFunc(text, func(match string) string {
|
|
if len(match) > 2 {
|
|
return ""
|
|
}
|
|
return match
|
|
})
|
|
return escapePattern.ReplaceAllString(stripped, `[$1$2]`)
|
|
}
|