2
0
mirror of https://github.com/rivo/tview.git synced 2024-11-15 06:12:46 +00:00

merge https://github.com/rivo/tview into list-spanhighlight, resolving conflicts for rivo/tview#220

This commit is contained in:
ardnew 2019-02-20 11:06:48 -06:00
commit c43d107310
8 changed files with 355 additions and 136 deletions

View File

@ -15,7 +15,7 @@ Please note that this document is work in progress so I might add to it in the f
## Pull Requests
If you have a feature request, open an issue first before sending me a pull request. It may save you from writing code that will get rejected. If your case is strong, there is a good chance that I will add the feature for you.
If you have a feature request, open an issue first before sending me a pull request, and allow for some discussion. It may save you from writing code that will get rejected. If your case is strong, there is a good chance that I will add the feature for you.
I'm very picky about the code that goes into this repo. So if you violate any of the following guidelines, there is a good chance I won't merge your pull request.
@ -26,6 +26,6 @@ I'm very picky about the code that goes into this repo. So if you violate any of
- Your code must follow the structure of the existing code. Don't just patch something on. Try to understand how `tview` is currently designed and follow that design. Your code needs to be consistent with existing code.
- If you're adding code that increases the work required to maintain the project, you must be willing to take responsibility for that extra work. I will ask you to maintain your part of the code in the long run.
- Function/type/variable/constant names must be as descriptive as they are right now. Follow the conventions of the package.
- All functions/types/variables/constants, even private ones, must have comments in good English. These comments must be elaborate enough so that new users of the package understand them and can follow them. Provide examples if you have to.
- All functions/types/variables/constants, even private ones, must have comments in good English. These comments must be elaborate enough so that new users of the package understand them and can follow them. Provide examples if you have to. Start all sentences upper-case, as is common in English, and end them with a period.
- Your changes must not decrease the project's [Go Report](https://goreportcard.com/report/github.com/rivo/tview) rating.
- No breaking changes unless there is absolutely no other way.

View File

@ -67,6 +67,10 @@ type DropDown struct {
// A callback function set by the Form class and called when the user leaves
// this form item.
finished func(tcell.Key)
// A callback function which is called when the user changes the drop-down's
// selection.
selected func(text string, index int)
}
// NewDropDown returns a new drop-down.
@ -203,13 +207,19 @@ func (d *DropDown) SetOptions(texts []string, selected func(text string, index i
d.options = nil
for index, text := range texts {
func(t string, i int) {
d.AddOption(text, func() {
if selected != nil {
selected(t, i)
}
})
d.AddOption(text, nil)
}(text, index)
}
d.selected = selected
return d
}
// SetSelectedFunc sets a handler which is called when the user changes the
// drop-down's option. This handler will be called in addition and prior to
// an option's optional individual handler. The handler is provided with the
// selected option's text and index.
func (d *DropDown) SetSelectedFunc(handler func(text string, index int)) *DropDown {
d.selected = handler
return d
}
@ -362,6 +372,9 @@ func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
d.currentOption = index
// Trigger "selected" event.
if d.selected != nil {
d.selected(d.options[d.currentOption].Text, d.currentOption)
}
if d.options[d.currentOption].Selected != nil {
d.options[d.currentOption].Selected()
}

64
grid.go
View File

@ -75,28 +75,28 @@ func NewGrid() *Grid {
return g
}
// SetRows defines how the rows of the grid are distributed. Each value defines
// the size of one row, starting with the leftmost row. Values greater 0
// represent absolute row widths (gaps not included). Values less or equal 0
// represent proportional row widths or fractions of the remaining free space,
// where 0 is treated the same as -1. That is, a row with a value of -3 will
// have three times the width of a row with a value of -1 (or 0). The minimum
// width set with SetMinSize() is always observed.
// SetColumns defines how the columns of the grid are distributed. Each value
// defines the size of one column, starting with the leftmost column. Values
// greater 0 represent absolute column widths (gaps not included). Values less
// or equal 0 represent proportional column widths or fractions of the remaining
// free space, where 0 is treated the same as -1. That is, a column with a value
// of -3 will have three times the width of a column with a value of -1 (or 0).
// The minimum width set with SetMinSize() is always observed.
//
// Primitives may extend beyond the rows defined explicitly with this function.
// A value of 0 is assumed for any undefined row. In fact, if you never call
// this function, all rows occupied by primitives will have the same width.
// On the other hand, unoccupied rows defined with this function will always
// take their place.
// Primitives may extend beyond the columns defined explicitly with this
// function. A value of 0 is assumed for any undefined column. In fact, if you
// never call this function, all columns occupied by primitives will have the
// same width. On the other hand, unoccupied columns defined with this function
// will always take their place.
//
// Assuming a total width of the grid of 100 cells and a minimum width of 0, the
// following call will result in rows with widths of 30, 10, 15, 15, and 30
// following call will result in columns with widths of 30, 10, 15, 15, and 30
// cells:
//
// grid.SetRows(30, 10, -1, -1, -2)
// grid.Setcolumns(30, 10, -1, -1, -2)
//
// If a primitive were then placed in the 6th and 7th row, the resulting widths
// would be: 30, 10, 10, 10, 20, 10, and 10 cells.
// If a primitive were then placed in the 6th and 7th column, the resulting
// widths would be: 30, 10, 10, 10, 20, 10, and 10 cells.
//
// If you then called SetMinSize() as follows:
//
@ -104,19 +104,19 @@ func NewGrid() *Grid {
//
// The resulting widths would be: 30, 15, 15, 15, 20, 15, and 15 cells, a total
// of 125 cells, 25 cells wider than the available grid width.
func (g *Grid) SetRows(rows ...int) *Grid {
g.rows = rows
func (g *Grid) SetColumns(columns ...int) *Grid {
g.columns = columns
return g
}
// SetColumns defines how the columns of the grid are distributed. These values
// behave the same as the row values provided with SetRows(), see there for
// a definition and examples.
// SetRows defines how the rows of the grid are distributed. These values behave
// the same as the column values provided with SetColumns(), see there for a
// definition and examples.
//
// The provided values correspond to column heights, the first value defining
// the height of the topmost column.
func (g *Grid) SetColumns(columns ...int) *Grid {
g.columns = columns
// The provided values correspond to row heights, the first value defining
// the height of the topmost row.
func (g *Grid) SetRows(rows ...int) *Grid {
g.rows = rows
return g
}
@ -171,12 +171,12 @@ func (g *Grid) SetBordersColor(color tcell.Color) *Grid {
// AddItem adds a primitive and its position to the grid. The top-left corner
// of the primitive will be located in the top-left corner of the grid cell at
// the given row and column and will span "width" rows and "height" columns. For
// example, for a primitive to occupy rows 2, 3, and 4 and columns 5 and 6:
// the given row and column and will span "rowSpan" rows and "colSpan" columns.
// For example, for a primitive to occupy rows 2, 3, and 4 and columns 5 and 6:
//
// grid.AddItem(p, 2, 4, 3, 2, true)
// grid.AddItem(p, 2, 5, 3, 2, true)
//
// If width or height is 0, the primitive will not be drawn.
// If rowSpan or colSpan is 0, the primitive will not be drawn.
//
// You can add the same primitive multiple times with different grid positions.
// The minGridWidth and minGridHeight values will then determine which of those
@ -195,13 +195,13 @@ func (g *Grid) SetBordersColor(color tcell.Color) *Grid {
// If the item's focus is set to true, it will receive focus when the grid
// receives focus. If there are multiple items with a true focus flag, the last
// visible one that was added will receive focus.
func (g *Grid) AddItem(p Primitive, row, column, height, width, minGridHeight, minGridWidth int, focus bool) *Grid {
func (g *Grid) AddItem(p Primitive, row, column, rowSpan, colSpan, minGridHeight, minGridWidth int, focus bool) *Grid {
g.items = append(g.items, &gridItem{
Item: p,
Row: row,
Column: column,
Height: height,
Width: width,
Height: rowSpan,
Width: colSpan,
MinGridHeight: minGridHeight,
MinGridWidth: minGridWidth,
Focus: focus,

187
list.go
View File

@ -2,6 +2,7 @@ package tview
import (
"fmt"
"strings"
"github.com/gdamore/tcell"
)
@ -50,6 +51,9 @@ type List struct {
// If true, the entire row is highlighted when selected.
highlightFullLine bool
// The number of list items skipped at the top before the first item is drawn.
offset int
// An optional function which is called when the user has navigated to a list
// item.
changed func(index int, mainText, secondaryText string, shortcut rune)
@ -75,32 +79,82 @@ func NewList() *List {
}
}
// SetCurrentItem sets the currently selected item by its index. This triggers
// a "changed" event.
// SetCurrentItem sets the currently selected item by its index, starting at 0
// for the first item. If a negative index is provided, items are referred to
// from the back (-1 = last item, -2 = second-to-last item, and so on). Out of
// range indices are clamped to the beginning/end.
//
// Calling this function triggers a "changed" event if the selection changes.
func (l *List) SetCurrentItem(index int) *List {
if index < 0 {
index = len(l.items) + index
}
if index >= len(l.items) {
index = len(l.items) - 1
}
if index < 0 {
index = 0
}
l.currentItem = index
if l.currentItem < len(l.items) && l.changed != nil {
if index != l.currentItem && l.changed != nil {
item := l.items[l.currentItem]
l.changed(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
}
return l
}
// GetCurrentItem returns the index of the currently selected list item.
// GetCurrentItem returns the index of the currently selected list item,
// starting at 0 for the first item.
func (l *List) GetCurrentItem() int {
return l.currentItem
}
// RemoveItem removes the item with the given index (starting at 0) from the
// list. Does nothing if the index is out of range.
// list. If a negative index is provided, items are referred to from the back
// (-1 = last item, -2 = second-to-last item, and so on). Out of range indices
// are clamped to the beginning/end, i.e. unless the list is empty, an item is
// always removed.
//
// The currently selected item is shifted accordingly. If it is the one that is
// removed, a "changed" event is fired.
func (l *List) RemoveItem(index int) *List {
if index < 0 || index >= len(l.items) {
if len(l.items) == 0 {
return l
}
l.items = append(l.items[:index], l.items[index+1:]...)
if l.currentItem >= len(l.items) {
l.currentItem = len(l.items) - 1
// Adjust index.
if index < 0 {
index = len(l.items) + index
}
if index >= len(l.items) {
index = len(l.items) - 1
}
if index < 0 {
index = 0
}
// Remove item.
l.items = append(l.items[:index], l.items[index+1:]...)
// If there is nothing left, we're done.
if len(l.items) == 0 {
return l
}
// Shift current item.
previousCurrentItem := l.currentItem
if l.currentItem >= index {
l.currentItem--
}
// Fire "changed" event for removed items.
if previousCurrentItem == index && l.changed != nil {
item := l.items[l.currentItem]
l.changed(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
}
return l
}
@ -184,28 +238,70 @@ func (l *List) SetDoneFunc(handler func()) *List {
return l
}
// AddItem adds a new item to the list. An item has a main text which will be
// highlighted when selected. It also has a secondary text which is shown
// underneath the main text (if it is set to visible) but which may remain
// empty.
// AddItem calls InsertItem() with an index of -1.
func (l *List) AddItem(mainText, secondaryText string, shortcut rune, selected func()) *List {
l.InsertItem(-1, mainText, secondaryText, shortcut, selected)
return l
}
// InsertItem adds a new item to the list at the specified index. An index of 0
// will insert the item at the beginning, an index of 1 before the second item,
// and so on. An index of GetItemCount() or higher will insert the item at the
// end of the list. Negative indices are also allowed: An index of -1 will
// insert the item at the end of the list, an index of -2 before the last item,
// and so on. An index of -GetItemCount()-1 or lower will insert the item at the
// beginning.
//
// An item has a main text which will be highlighted when selected. It also has
// a secondary text which is shown underneath the main text (if it is set to
// visible) but which may remain empty.
//
// The shortcut is a key binding. If the specified rune is entered, the item
// is selected immediately. Set to 0 for no binding.
//
// The "selected" callback will be invoked when the user selects the item. You
// may provide nil if no such item is needed or if all events are handled
// may provide nil if no such callback is needed or if all events are handled
// through the selected callback set with SetSelectedFunc().
func (l *List) AddItem(mainText, secondaryText string, shortcut rune, selected func()) *List {
l.items = append(l.items, &listItem{
//
// The currently selected item will shift its position accordingly. If the list
// was previously empty, a "changed" event is fired because the new item becomes
// selected.
func (l *List) InsertItem(index int, mainText, secondaryText string, shortcut rune, selected func()) *List {
item := &listItem{
MainText: mainText,
SecondaryText: secondaryText,
Shortcut: shortcut,
Selected: selected,
})
}
// Shift index to range.
if index < 0 {
index = len(l.items) + index + 1
}
if index < 0 {
index = 0
} else if index > len(l.items) {
index = len(l.items)
}
// Shift current item.
if l.currentItem < len(l.items) && l.currentItem >= index {
l.currentItem++
}
// Insert item (make space for the new item, then shift and insert).
l.items = append(l.items, nil)
if index < len(l.items)-1 { // -1 because l.items has already grown by one item.
copy(l.items[index+1:], l.items[index:])
}
l.items[index] = item
// Fire a "change" event for the first item in the list.
if len(l.items) == 1 && l.changed != nil {
item := l.items[0]
l.changed(0, item.MainText, item.SecondaryText, item.Shortcut)
}
return l
}
@ -229,6 +325,46 @@ func (l *List) SetItemText(index int, main, secondary string) *List {
return l
}
// FindItems searches the main and secondary texts for the given strings and
// returns a list of item indices in which those strings are found. One of the
// two search strings may be empty, it will then be ignored. Indices are always
// returned in ascending order.
//
// If mustContainBoth is set to true, mainSearch must be contained in the main
// text AND secondarySearch must be contained in the secondary text. If it is
// false, only one of the two search strings must be contained.
//
// Set ignoreCase to true for case-insensitive search.
func (l *List) FindItems(mainSearch, secondarySearch string, mustContainBoth, ignoreCase bool) (indices []int) {
if mainSearch == "" && secondarySearch == "" {
return
}
if ignoreCase {
mainSearch = strings.ToLower(mainSearch)
secondarySearch = strings.ToLower(secondarySearch)
}
for index, item := range l.items {
mainText := item.MainText
secondaryText := item.SecondaryText
if ignoreCase {
mainText = strings.ToLower(mainText)
secondaryText = strings.ToLower(secondaryText)
}
// strings.Contains() always returns true for a "" search.
mainContained := strings.Contains(mainText, mainSearch)
secondaryContained := strings.Contains(secondaryText, secondarySearch)
if mustContainBoth && mainContained && secondaryContained ||
!mustContainBoth && (mainText != "" && mainContained || secondaryText != "" && secondaryContained) {
indices = append(indices, index)
}
}
return
}
// Clear removes all items from the list.
func (l *List) Clear() *List {
l.items = nil
@ -255,21 +391,22 @@ func (l *List) Draw(screen tcell.Screen) {
}
}
// We want to keep the current selection in view. What is our offset?
var offset int
if l.showSecondaryText {
if 2*l.currentItem >= height {
offset = (2*l.currentItem + 2 - height) / 2
// Adjust offset to keep the current selection in view.
if l.currentItem < l.offset {
l.offset = l.currentItem
} else if l.showSecondaryText {
if 2*(l.currentItem-l.offset) >= height-1 {
l.offset = (2*l.currentItem + 3 - height) / 2
}
} else {
if l.currentItem >= height {
offset = l.currentItem + 1 - height
if l.currentItem-l.offset >= height {
l.offset = l.currentItem + 1 - height
}
}
// Draw the list items.
for index, item := range l.items {
if index < offset {
if index < l.offset {
continue
}

View File

@ -11,6 +11,9 @@ import (
// directly but all colors (background and text) will be set to their default
// which is black.
type TableCell struct {
// The reference object.
Reference interface{}
// The text to be displayed in the table cell.
Text string
@ -132,6 +135,19 @@ func (c *TableCell) SetSelectable(selectable bool) *TableCell {
return c
}
// SetReference allows you to store a reference of any type in this cell. This
// will allow you to establish a mapping between the cell and your
// actual data.
func (c *TableCell) SetReference(reference interface{}) *TableCell {
c.Reference = reference
return c
}
// GetReference returns this cell's reference object.
func (c *TableCell) GetReference() interface{} {
return c.Reference
}
// GetLastPosition returns the position of the table cell the last time it was
// drawn on screen. If the cell is not on screen, the return values are
// undefined.
@ -423,7 +439,9 @@ func (t *Table) SetCellSimple(row, column int, text string) *Table {
// GetCell returns the contents of the cell at the specified position. A valid
// TableCell object is always returned but it will be uninitialized if the cell
// was not previously set.
// was not previously set. Such an uninitialized object will not automatically
// be inserted. Therefore, repeated calls to this function may return different
// pointers for uninitialized cells.
func (t *Table) GetCell(row, column int) *TableCell {
if row >= len(t.cells) || column >= len(t.cells[row]) {
return &TableCell{}
@ -456,6 +474,35 @@ func (t *Table) RemoveColumn(column int) *Table {
return t
}
// InsertRow inserts a row before the row with the given index. Cells on the
// given row and below will be shifted to the bottom by one row. If "row" is
// equal or larger than the current number of rows, this function has no effect.
func (t *Table) InsertRow(row int) *Table {
if row >= len(t.cells) {
return t
}
t.cells = append(t.cells, nil) // Extend by one.
copy(t.cells[row+1:], t.cells[row:]) // Shift down.
t.cells[row] = nil // New row is uninitialized.
return t
}
// InsertColumn inserts a column before the column with the given index. Cells
// in the given column and to its right will be shifted to the right by one
// column. Rows that have fewer initialized cells than "column" will remain
// unchanged.
func (t *Table) InsertColumn(column int) *Table {
for row := range t.cells {
if column >= len(t.cells[row]) {
continue
}
t.cells[row] = append(t.cells[row], nil) // Extend by one.
copy(t.cells[row][column+1:], t.cells[row][column:]) // Shift to the right.
t.cells[row][column] = &TableCell{} // New element is an uninitialized table cell.
}
return t
}
// GetRowCount returns the number of rows in the table.
func (t *Table) GetRowCount() int {
return len(t.cells)
@ -650,7 +697,7 @@ ColumnLoop:
expansion := 0
for _, row := range rows {
if cell := getCell(row, column); cell != nil {
_, _, _, _, cellWidth := decomposeString(cell.Text)
_, _, _, _, _, _, cellWidth := decomposeString(cell.Text, true, false)
if cell.MaxWidth > 0 && cell.MaxWidth < cellWidth {
cellWidth = cell.MaxWidth
}

View File

@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"regexp"
"strings"
"sync"
"unicode/utf8"
@ -12,8 +13,14 @@ import (
runewidth "github.com/mattn/go-runewidth"
)
// TabSize is the number of spaces with which a tab character will be replaced.
var TabSize = 4
var (
openColorRegex = regexp.MustCompile(`\[([a-zA-Z]*|#[0-9a-zA-Z]*)$`)
openRegionRegex = regexp.MustCompile(`\["[a-zA-Z0-9_,;: \-\.]*"?$`)
newLineRegex = regexp.MustCompile(`\r?\n`)
// TabSize is the number of spaces with which a tab character will be replaced.
TabSize = 4
)
// textViewIndex contains information about each line displayed in the text
// view.
@ -239,6 +246,34 @@ func (t *TextView) SetText(text string) *TextView {
return t
}
// GetText returns the current text of this text view. If "stripTags" is set
// to true, any region/color tags are stripped from the text.
func (t *TextView) GetText(stripTags bool) string {
// Get the buffer.
buffer := t.buffer
if !stripTags {
buffer = append(buffer, string(t.recentBytes))
}
// Add newlines again.
text := strings.Join(buffer, "\n")
// Strip from tags if required.
if stripTags {
if t.regions {
text = regionPattern.ReplaceAllString(text, "")
}
if t.dynamicColors {
text = colorPattern.ReplaceAllString(text, "")
}
if t.regions || t.dynamicColors {
text = escapePattern.ReplaceAllString(text, `[$1$2]`)
}
}
return text
}
// SetDynamicColors sets the flag that allows the text color to be changed
// dynamically. See class description for details.
func (t *TextView) SetDynamicColors(dynamic bool) *TextView {
@ -497,8 +532,7 @@ func (t *TextView) Write(p []byte) (n int, err error) {
// If we have a trailing open dynamic color, exclude it.
if t.dynamicColors {
openColor := regexp.MustCompile(`\[([a-zA-Z]*|#[0-9a-zA-Z]*)$`)
location := openColor.FindIndex(newBytes)
location := openColorRegex.FindIndex(newBytes)
if location != nil {
t.recentBytes = newBytes[location[0]:]
newBytes = newBytes[:location[0]]
@ -507,8 +541,7 @@ func (t *TextView) Write(p []byte) (n int, err error) {
// If we have a trailing open region, exclude it.
if t.regions {
openRegion := regexp.MustCompile(`\["[a-zA-Z0-9_,;: \-\.]*"?$`)
location := openRegion.FindIndex(newBytes)
location := openRegionRegex.FindIndex(newBytes)
if location != nil {
t.recentBytes = newBytes[location[0]:]
newBytes = newBytes[:location[0]]
@ -516,9 +549,8 @@ func (t *TextView) Write(p []byte) (n int, err error) {
}
// Transform the new bytes into strings.
newLine := regexp.MustCompile(`\r?\n`)
newBytes = bytes.Replace(newBytes, []byte{'\t'}, bytes.Repeat([]byte{' '}, TabSize), -1)
for index, line := range newLine.Split(string(newBytes), -1) {
for index, line := range newLineRegex.Split(string(newBytes), -1) {
if index == 0 {
if len(t.buffer) == 0 {
t.buffer = []string{line}
@ -558,33 +590,11 @@ func (t *TextView) reindexBuffer(width int) {
// Go through each line in the buffer.
for bufferIndex, str := range t.buffer {
// Find all color tags in this line. Then remove them.
var (
colorTagIndices [][]int
colorTags [][]string
escapeIndices [][]int
)
strippedStr := str
if t.dynamicColors {
colorTagIndices, colorTags, escapeIndices, strippedStr, _ = decomposeString(str)
}
// Find all regions in this line. Then remove them.
var (
regionIndices [][]int
regions [][]string
)
if t.regions {
regionIndices = regionPattern.FindAllStringIndex(str, -1)
regions = regionPattern.FindAllStringSubmatch(str, -1)
strippedStr = regionPattern.ReplaceAllString(strippedStr, "")
}
// We don't need the original string anymore for now.
str = strippedStr
colorTagIndices, colorTags, regionIndices, regions, escapeIndices, strippedStr, _ := decomposeString(str, t.dynamicColors, t.regions)
// Split the line if required.
var splitLines []string
str = strippedStr
if t.wrap && len(str) > 0 {
for len(str) > 0 {
extract := runewidth.Truncate(str, width, "")
@ -829,31 +839,8 @@ func (t *TextView) Draw(screen tcell.Screen) {
attributes := index.Attributes
regionID := index.Region
// Get color tags.
var (
colorTagIndices [][]int
colorTags [][]string
escapeIndices [][]int
)
strippedText := text
if t.dynamicColors {
colorTagIndices, colorTags, escapeIndices, strippedText, _ = decomposeString(text)
}
// Get regions.
var (
regionIndices [][]int
regions [][]string
)
if t.regions {
regionIndices = regionPattern.FindAllStringIndex(text, -1)
regions = regionPattern.FindAllStringSubmatch(text, -1)
strippedText = regionPattern.ReplaceAllString(strippedText, "")
if !t.dynamicColors {
escapeIndices = escapePattern.FindAllStringIndex(text, -1)
strippedText = string(escapePattern.ReplaceAllString(strippedText, "[$1$2]"))
}
}
// Process tags.
colorTagIndices, colorTags, regionIndices, regions, escapeIndices, strippedText, _ := decomposeString(text, t.dynamicColors, t.regions)
// Calculate the position of the line.
var skip, posX int

View File

@ -105,6 +105,11 @@ func (n *TreeNode) SetChildren(childNodes []*TreeNode) *TreeNode {
return n
}
// GetText returns this node's text.
func (n *TreeNode) GetText() string {
return n.text
}
// GetChildren returns this node's children.
func (n *TreeNode) GetChildren() []*TreeNode {
return n.children

56
util.go
View File

@ -3,6 +3,7 @@ package tview
import (
"math"
"regexp"
"sort"
"strconv"
"unicode"
@ -160,15 +161,28 @@ func overlayStyle(background tcell.Color, defaultStyle tcell.Style, fgColor, bgC
}
// decomposeString returns information about a string which may contain color
// tags. It returns the indices of the color tags (as returned by
// 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 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)
// 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, runewidth.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.
@ -179,10 +193,26 @@ func decomposeString(text string) (colorIndices [][]int, colors [][]string, esca
}
}
// Remove the color tags from the original string.
// Make a (sorted) list of all tags.
var allIndices [][]int
if findColors && findRegions {
allIndices = colorIndices
allIndices = make([][]int, len(colorIndices)+len(regionIndices))
copy(allIndices, colorIndices)
copy(allIndices[len(colorIndices):], regionIndices)
sort.Slice(allIndices, func(i int, j int) bool {
return allIndices[i][0] < allIndices[j][0]
})
} else if findColors {
allIndices = colorIndices
} else {
allIndices = regionIndices
}
// Remove the tags from the original string.
var from int
buf := make([]byte, 0, len(text))
for _, indices := range colorIndices {
for _, indices := range allIndices {
buf = append(buf, []byte(text[from:indices[0]])...)
from = indices[1]
}
@ -218,7 +248,7 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int,
}
// Decompose the text.
colorIndices, colors, escapeIndices, strippedText, strippedWidth := decomposeString(text)
colorIndices, colors, _, _, escapeIndices, strippedText, strippedWidth := decomposeString(text, true, false)
// We want to reduce all alignments to AlignLeft.
if align == AlignRight {
@ -382,7 +412,7 @@ 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 {
_, _, _, _, width := decomposeString(text)
_, _, _, _, _, _, width := decomposeString(text, true, false)
return width
}
@ -394,7 +424,7 @@ func StringWidth(text string) int {
//
// Text is always split at newline characters ('\n').
func WordWrap(text string, width int) (lines []string) {
colorTagIndices, _, escapeIndices, strippedText, _ := decomposeString(text)
colorTagIndices, _, _, _, escapeIndices, strippedText, _ := decomposeString(text, true, false)
// Find candidate breakpoints.
breakpoints := boundaryPattern.FindAllStringSubmatchIndex(strippedText, -1)