1
0
mirror of https://github.com/taigrr/wtf synced 2026-04-01 20:38:43 -07:00

Update dependencies

This commit is contained in:
Chris Cummer
2018-10-21 12:48:22 -07:00
parent 8f3ae94b4e
commit 3a0bcd21e7
132 changed files with 18033 additions and 9138 deletions

View File

@@ -65,6 +65,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.18 (2018-10-18)
- `InputField` elements can now be navigated freely.
- v0.17 (2018-06-20)
- Added `TreeView`.
- v0.15 (2018-05-02)

View File

@@ -19,6 +19,9 @@ type Application struct {
// The application's screen.
screen tcell.Screen
// Indicates whether the application's screen is currently active.
running bool
// The primitive which currently has the keyboard focus.
focus Primitive
@@ -70,22 +73,53 @@ func (a *Application) GetInputCapture() func(event *tcell.EventKey) *tcell.Event
return a.inputCapture
}
// SetScreen allows you to provide your own tcell.Screen object. For most
// applications, this is not needed and you should be familiar with
// tcell.Screen when using this function. Run() will call Init() and Fini() on
// the provided screen object.
//
// This function is typically called before calling Run(). Calling it while an
// application is running will switch the application to the new screen. Fini()
// will be called on the old screen and Init() on the new screen (errors
// returned by Init() will lead to a panic).
//
// Note that calling Suspend() will invoke Fini() on your screen object and it
// will not be restored when suspended mode ends. Instead, a new default screen
// object will be created.
func (a *Application) SetScreen(screen tcell.Screen) *Application {
a.Lock()
defer a.Unlock()
if a.running {
a.screen.Fini()
}
a.screen = screen
if a.running {
if err := a.screen.Init(); err != nil {
panic(err)
}
}
return a
}
// Run starts the application and thus the event loop. This function returns
// when Stop() was called.
func (a *Application) Run() error {
var err error
a.Lock()
// Make a screen.
a.screen, err = tcell.NewScreen()
if err != nil {
a.Unlock()
return err
// Make a screen if there is none yet.
if a.screen == nil {
a.screen, err = tcell.NewScreen()
if err != nil {
a.Unlock()
return err
}
}
if err = a.screen.Init(); err != nil {
a.Unlock()
return err
}
a.running = true
// We catch panics to clean up because they mess up the terminal.
defer func() {
@@ -93,6 +127,7 @@ func (a *Application) Run() error {
if a.screen != nil {
a.screen.Fini()
}
a.running = false
panic(p)
}
}()
@@ -170,6 +205,7 @@ func (a *Application) Stop() {
}
a.screen.Fini()
a.screen = nil
a.running = false
}
// Suspend temporarily suspends the application by exiting terminal UI mode and
@@ -217,6 +253,7 @@ func (a *Application) Suspend(f func()) bool {
a.Unlock()
panic(err)
}
a.running = true
a.Unlock()
a.Draw()

40
vendor/github.com/rivo/tview/box.go generated vendored
View File

@@ -51,10 +51,6 @@ type Box struct {
// Whether or not this box has focus.
hasFocus bool
// If set to true, the inner rect of this box will be within the screen at the
// last time the box was drawn.
clampToScreen bool
// An optional capture function which receives a key event and returns the
// event to be forwarded to the primitive's default input handler (nil if
// nothing should be forwarded).
@@ -74,7 +70,6 @@ func NewBox() *Box {
borderColor: Styles.BorderColor,
titleColor: Styles.TitleColor,
titleAlign: AlignCenter,
clampToScreen: true,
}
b.focus = b
return b
@@ -117,6 +112,7 @@ func (b *Box) SetRect(x, y, width, height int) {
b.y = y
b.width = width
b.height = height
b.innerX = -1 // Mark inner rect as uninitialized.
}
// SetDrawFunc sets a callback function which is invoked after the box primitive
@@ -277,8 +273,8 @@ func (b *Box) Draw(screen tcell.Screen) {
// Draw title.
if b.title != "" && b.width >= 4 {
_, printed := Print(screen, b.title, b.x+1, b.y, b.width-2, b.titleAlign, b.titleColor)
if StringWidth(b.title)-printed > 0 && printed > 0 {
printed, _ := Print(screen, b.title, b.x+1, b.y, b.width-2, b.titleAlign, b.titleColor)
if len(b.title)-printed > 0 && printed > 0 {
_, _, style, _ := screen.GetContent(b.x+b.width-2, b.y)
fg, _, _ := style.Decompose()
Print(screen, string(SemigraphicsHorizontalEllipsis), b.x+b.width-2, b.y, 1, AlignLeft, fg)
@@ -296,22 +292,20 @@ func (b *Box) Draw(screen tcell.Screen) {
}
// Clamp inner rect to screen.
if b.clampToScreen {
width, height := screen.Size()
if b.innerX < 0 {
b.innerWidth += b.innerX
b.innerX = 0
}
if b.innerX+b.innerWidth >= width {
b.innerWidth = width - b.innerX
}
if b.innerY+b.innerHeight >= height {
b.innerHeight = height - b.innerY
}
if b.innerY < 0 {
b.innerHeight += b.innerY
b.innerY = 0
}
width, height := screen.Size()
if b.innerX < 0 {
b.innerWidth += b.innerX
b.innerX = 0
}
if b.innerX+b.innerWidth >= width {
b.innerWidth = width - b.innerX
}
if b.innerY+b.innerHeight >= height {
b.innerHeight = height - b.innerY
}
if b.innerY < 0 {
b.innerHeight += b.innerY
b.innerY = 0
}
}

View File

@@ -124,9 +124,9 @@ func (c *Checkbox) SetChangedFunc(handler func(checked bool)) *Checkbox {
return c
}
// SetDoneFunc sets a handler which is called when the user is done entering
// text. The callback function is provided with the key that was pressed, which
// is one of the following:
// SetDoneFunc sets a handler which is called when the user is done using the
// checkbox. The callback function is provided with the key that was pressed,
// which is one of the following:
//
// - KeyEscape: Abort text input.
// - KeyTab: Move to the next field.

12
vendor/github.com/rivo/tview/form.go generated vendored
View File

@@ -218,6 +218,13 @@ func (f *Form) AddButton(label string, selected func()) *Form {
return f
}
// GetButton returns the button at the specified 0-based index. Note that
// buttons have been specially prepared for this form and modifying some of
// their attributes may have unintended side effects.
func (f *Form) GetButton(index int) *Button {
return f.buttons[index]
}
// RemoveButton removes the button at the specified position, starting with 0
// for the button that was added first.
func (f *Form) RemoveButton(index int) *Form {
@@ -225,6 +232,11 @@ func (f *Form) RemoveButton(index int) *Form {
return f
}
// GetButtonCount returns the number of buttons in this form.
func (f *Form) GetButtonCount() int {
return len(f.buttons)
}
// GetButtonIndex returns the index of the button with the given label, starting
// with 0 for the button that was added first. If no such label was found, -1
// is returned.

View File

@@ -11,10 +11,23 @@ import (
)
// InputField is a one-line box (three lines if there is a title) where the
// user can enter text.
// user can enter text. Use SetAcceptanceFunc() to accept or reject input,
// SetChangedFunc() to listen for changes, and SetMaskCharacter() to hide input
// from onlookers (e.g. for password input).
//
// Use SetMaskCharacter() to hide input from onlookers (e.g. for password
// input).
// The following keys can be used for navigation and editing:
//
// - Left arrow: Move left by one character.
// - Right arrow: Move right by one character.
// - Home, Ctrl-A, Alt-a: Move to the beginning of the line.
// - End, Ctrl-E, Alt-e: Move to the end of the line.
// - Alt-left, Alt-b: Move left by one word.
// - Alt-right, Alt-f: Move right by one word.
// - Backspace: Delete the character before the cursor.
// - Delete: Delete the character after the cursor.
// - Ctrl-K: Delete from the cursor to the end of the line.
// - Ctrl-W: Delete the last word before the cursor.
// - Ctrl-U: Delete the entire line.
//
// See https://github.com/rivo/tview/wiki/InputField for an example.
type InputField struct {
@@ -53,6 +66,12 @@ type InputField struct {
// disables masking.
maskCharacter rune
// The cursor position as a byte index into the text string.
cursorPos int
// The number of bytes of the text string skipped ahead while drawing.
offset int
// An optional function which may reject the last character that was entered.
accept func(text string, ch rune) bool
@@ -174,7 +193,7 @@ func (i *InputField) SetMaskCharacter(mask rune) *InputField {
// SetAcceptanceFunc sets a handler which may reject the last character that was
// entered (by returning false).
//
// This package defines a number of variables Prefixed with InputField which may
// This package defines a number of variables prefixed with InputField which may
// be used for common input (e.g. numbers, maximum text length).
func (i *InputField) SetAcceptanceFunc(handler func(textToCheck string, lastChar rune) bool) *InputField {
i.accept = handler
@@ -244,56 +263,69 @@ func (i *InputField) Draw(screen tcell.Screen) {
screen.SetContent(x+index, y, ' ', nil, fieldStyle)
}
// Draw placeholder text.
// Text.
var cursorScreenPos int
text := i.text
if text == "" && i.placeholder != "" {
Print(screen, i.placeholder, x, y, fieldWidth, AlignLeft, i.placeholderTextColor)
// Draw placeholder text.
Print(screen, Escape(i.placeholder), x, y, fieldWidth, AlignLeft, i.placeholderTextColor)
i.offset = 0
} else {
// Draw entered text.
if i.maskCharacter > 0 {
text = strings.Repeat(string(i.maskCharacter), utf8.RuneCountInString(i.text))
} else {
text = Escape(text)
}
fieldWidth-- // We need one cell for the cursor.
if fieldWidth < runewidth.StringWidth(text) {
Print(screen, text, x, y, fieldWidth, AlignRight, i.fieldTextColor)
stringWidth := runewidth.StringWidth(text)
if fieldWidth >= stringWidth {
// We have enough space for the full text.
Print(screen, Escape(text), x, y, fieldWidth, AlignLeft, i.fieldTextColor)
i.offset = 0
iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
if textPos >= i.cursorPos {
return true
}
cursorScreenPos += screenWidth
return false
})
} else {
Print(screen, text, x, y, fieldWidth, AlignLeft, i.fieldTextColor)
// The text doesn't fit. Where is the cursor?
if i.cursorPos < 0 {
i.cursorPos = 0
} else if i.cursorPos > len(text) {
i.cursorPos = len(text)
}
// Shift the text so the cursor is inside the field.
var shiftLeft int
if i.offset > i.cursorPos {
i.offset = i.cursorPos
} else if subWidth := runewidth.StringWidth(text[i.offset:i.cursorPos]); subWidth > fieldWidth-1 {
shiftLeft = subWidth - fieldWidth + 1
}
currentOffset := i.offset
iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
if textPos >= currentOffset {
if shiftLeft > 0 {
i.offset = textPos + textWidth
shiftLeft -= screenWidth
} else {
if textPos+textWidth > i.cursorPos {
return true
}
cursorScreenPos += screenWidth
}
}
return false
})
Print(screen, Escape(text[i.offset:]), x, y, fieldWidth, AlignLeft, i.fieldTextColor)
}
}
// Set cursor.
if i.focus.HasFocus() {
i.setCursor(screen)
screen.ShowCursor(x+cursorScreenPos, y)
}
}
// setCursor sets the cursor position.
func (i *InputField) setCursor(screen tcell.Screen) {
x := i.x
y := i.y
rightLimit := x + i.width
if i.border {
x++
y++
rightLimit -= 2
}
fieldWidth := runewidth.StringWidth(i.text)
if i.fieldWidth > 0 && fieldWidth > i.fieldWidth-1 {
fieldWidth = i.fieldWidth - 1
}
if i.labelWidth > 0 {
x += i.labelWidth + fieldWidth
} else {
x += StringWidth(i.label) + fieldWidth
}
if x >= rightLimit {
x = rightLimit - 1
}
screen.ShowCursor(x, y)
}
// InputHandler returns the handler for this primitive.
func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return i.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
@@ -305,27 +337,95 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p
}
}()
// Movement functions.
home := func() { i.cursorPos = 0 }
end := func() { i.cursorPos = len(i.text) }
moveLeft := func() {
iterateStringReverse(i.text[:i.cursorPos], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
i.cursorPos -= textWidth
return true
})
}
moveRight := func() {
iterateString(i.text[i.cursorPos:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
i.cursorPos += textWidth
return true
})
}
moveWordLeft := func() {
i.cursorPos = len(regexp.MustCompile(`\S+\s*$`).ReplaceAllString(i.text[:i.cursorPos], ""))
}
moveWordRight := func() {
i.cursorPos = len(i.text) - len(regexp.MustCompile(`^\s*\S+\s*`).ReplaceAllString(i.text[i.cursorPos:], ""))
}
// Process key event.
switch key := event.Key(); key {
case tcell.KeyRune: // Regular character.
newText := i.text + string(event.Rune())
if i.accept != nil {
if !i.accept(newText, event.Rune()) {
break
modifiers := event.Modifiers()
if modifiers == tcell.ModNone {
ch := string(event.Rune())
newText := i.text[:i.cursorPos] + ch + i.text[i.cursorPos:]
if i.accept != nil {
if !i.accept(newText, event.Rune()) {
break
}
}
i.text = newText
i.cursorPos += len(ch)
} else if modifiers&tcell.ModAlt > 0 {
// We accept some Alt- key combinations.
switch event.Rune() {
case 'a': // Home.
home()
case 'e': // End.
end()
case 'b': // Move word left.
moveWordLeft()
case 'f': // Move word right.
moveWordRight()
}
}
i.text = newText
case tcell.KeyCtrlU: // Delete all.
i.text = ""
i.cursorPos = 0
case tcell.KeyCtrlK: // Delete until the end of the line.
i.text = i.text[:i.cursorPos]
case tcell.KeyCtrlW: // Delete last word.
lastWord := regexp.MustCompile(`\s*\S+\s*$`)
i.text = lastWord.ReplaceAllString(i.text, "")
case tcell.KeyBackspace, tcell.KeyBackspace2: // Delete last character.
if len(i.text) == 0 {
break
lastWord := regexp.MustCompile(`\S+\s*$`)
newText := lastWord.ReplaceAllString(i.text[:i.cursorPos], "") + i.text[i.cursorPos:]
i.cursorPos -= len(i.text) - len(newText)
i.text = newText
case tcell.KeyBackspace, tcell.KeyBackspace2: // Delete character before the cursor.
iterateStringReverse(i.text[:i.cursorPos], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
i.text = i.text[:textPos] + i.text[textPos+textWidth:]
i.cursorPos -= textWidth
return true
})
if i.offset >= i.cursorPos {
i.offset = 0
}
runes := []rune(i.text)
i.text = string(runes[:len(runes)-1])
case tcell.KeyDelete: // Delete character after the cursor.
iterateString(i.text[i.cursorPos:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
i.text = i.text[:i.cursorPos] + i.text[i.cursorPos+textWidth:]
return true
})
case tcell.KeyLeft:
if event.Modifiers()&tcell.ModAlt > 0 {
moveWordLeft()
} else {
moveLeft()
}
case tcell.KeyRight:
if event.Modifiers()&tcell.ModAlt > 0 {
moveWordRight()
} else {
moveRight()
}
case tcell.KeyHome, tcell.KeyCtrlA:
home()
case tcell.KeyEnd, tcell.KeyCtrlE:
end()
case tcell.KeyEnter, tcell.KeyTab, tcell.KeyBacktab, tcell.KeyEscape: // We're done.
if i.done != nil {
i.done(key)

15
vendor/github.com/rivo/tview/list.go generated vendored
View File

@@ -85,6 +85,19 @@ 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.
func (l *List) RemoveItem(index int) *List {
if index < 0 || index >= len(l.items) {
return l
}
l.items = append(l.items[:index], l.items[index+1:]...)
if l.currentItem >= len(l.items) {
l.currentItem = len(l.items) - 1
}
return l
}
// SetMainTextColor sets the color of the items' main text.
func (l *List) SetMainTextColor(color tcell.Color) *List {
l.mainTextColor = color
@@ -127,7 +140,7 @@ func (l *List) ShowSecondaryText(show bool) *List {
//
// This function is also called when the first item is added or when
// SetCurrentItem() is called.
func (l *List) SetChangedFunc(handler func(int, string, string, rune)) *List {
func (l *List) SetChangedFunc(handler func(index int, mainText string, secondaryText string, shortcut rune)) *List {
l.changed = handler
return l
}

View File

@@ -86,6 +86,16 @@ func (m *Modal) AddButtons(labels []string) *Modal {
m.done(i, l)
}
})
button := m.form.GetButton(m.form.GetButtonCount() - 1)
button.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch event.Key() {
case tcell.KeyDown, tcell.KeyRight:
return tcell.NewEventKey(tcell.KeyTab, 0, tcell.ModNone)
case tcell.KeyUp, tcell.KeyLeft:
return tcell.NewEventKey(tcell.KeyBacktab, 0, tcell.ModNone)
}
return event
})
}(index, label)
}
return m

View File

@@ -231,6 +231,10 @@ type Table struct {
// The number of visible rows the last time the table was drawn.
visibleRows int
// The style of the selected rows. If this value is 0, selected rows are
// simply inverted.
selectedStyle tcell.Style
// An optional function which gets called when the user presses Enter on a
// selected cell. If entire rows selected, the column value is undefined.
// Likewise for entire columns.
@@ -276,6 +280,18 @@ func (t *Table) SetBordersColor(color tcell.Color) *Table {
return t
}
// SetSelectedStyle sets a specific style for selected cells. If no such style
// is set, per default, selected cells are inverted (i.e. their foreground and
// background colors are swapped).
//
// To reset a previous setting to its default, make the following call:
//
// table.SetSelectedStyle(tcell.ColorDefault, tcell.ColorDefault, 0)
func (t *Table) SetSelectedStyle(foregroundColor, backgroundColor tcell.Color, attributes tcell.AttrMask) *Table {
t.selectedStyle = tcell.StyleDefault.Foreground(foregroundColor).Background(backgroundColor) | tcell.Style(attributes)
return t
}
// SetSeparator sets the character used to fill the space between two
// neighboring cells. This is a space character ' ' per default but you may
// want to set it to Borders.Vertical (or any other rune) if the column
@@ -743,12 +759,17 @@ ColumnLoop:
}
// Helper function which colors the background of a box.
colorBackground := func(fromX, fromY, w, h int, backgroundColor, textColor tcell.Color, selected bool) {
// backgroundColor == tcell.ColorDefault => Don't color the background.
// textColor == tcell.ColorDefault => Don't change the text color.
// attr == 0 => Don't change attributes.
// invert == true => Ignore attr, set text to backgroundColor or t.backgroundColor;
// set background to textColor.
colorBackground := func(fromX, fromY, w, h int, backgroundColor, textColor tcell.Color, attr tcell.AttrMask, invert bool) {
for by := 0; by < h && fromY+by < y+height; by++ {
for bx := 0; bx < w && fromX+bx < x+width; bx++ {
m, c, style, _ := screen.GetContent(fromX+bx, fromY+by)
if selected {
fg, _, _ := style.Decompose()
fg, bg, a := style.Decompose()
if invert {
if fg == textColor || fg == t.bordersColor {
fg = backgroundColor
}
@@ -757,10 +778,16 @@ ColumnLoop:
}
style = style.Background(textColor).Foreground(fg)
} else {
if backgroundColor == tcell.ColorDefault {
continue
if backgroundColor != tcell.ColorDefault {
bg = backgroundColor
}
style = style.Background(backgroundColor)
if textColor != tcell.ColorDefault {
fg = textColor
}
if attr != 0 {
a = attr
}
style = style.Background(bg).Foreground(fg) | tcell.Style(a)
}
screen.SetContent(fromX+bx, fromY+by, m, c, style)
}
@@ -769,11 +796,12 @@ ColumnLoop:
// Color the cell backgrounds. To avoid undesirable artefacts, we combine
// the drawing of a cell by background color, selected cells last.
cellsByBackgroundColor := make(map[tcell.Color][]*struct {
type cellInfo struct {
x, y, w, h int
text tcell.Color
selected bool
})
}
cellsByBackgroundColor := make(map[tcell.Color][]*cellInfo)
var backgroundColors []tcell.Color
for rowY, row := range rows {
columnX := 0
@@ -793,11 +821,7 @@ ColumnLoop:
columnSelected := t.columnsSelectable && !t.rowsSelectable && column == t.selectedColumn
cellSelected := !cell.NotSelectable && (columnSelected || rowSelected || t.rowsSelectable && t.columnsSelectable && column == t.selectedColumn && row == t.selectedRow)
entries, ok := cellsByBackgroundColor[cell.BackgroundColor]
cellsByBackgroundColor[cell.BackgroundColor] = append(entries, &struct {
x, y, w, h int
text tcell.Color
selected bool
}{
cellsByBackgroundColor[cell.BackgroundColor] = append(entries, &cellInfo{
x: bx,
y: by,
w: bw,
@@ -821,13 +845,18 @@ ColumnLoop:
_, _, lj := c.Hcl()
return li < lj
})
selFg, selBg, selAttr := t.selectedStyle.Decompose()
for _, bgColor := range backgroundColors {
entries := cellsByBackgroundColor[bgColor]
for _, cell := range entries {
if cell.selected {
defer colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, cell.text, true)
if t.selectedStyle != 0 {
defer colorBackground(cell.x, cell.y, cell.w, cell.h, selBg, selFg, selAttr, false)
} else {
defer colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, cell.text, 0, true)
}
} else {
colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, cell.text, false)
colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, tcell.ColorDefault, 0, false)
}
}
}

View File

@@ -5,7 +5,6 @@ import (
"fmt"
"regexp"
"sync"
"unicode"
"unicode/utf8"
"github.com/gdamore/tcell"
@@ -549,12 +548,6 @@ func (t *TextView) reindexBuffer(width int) {
strippedStr = regionPattern.ReplaceAllString(strippedStr, "")
}
// Find all escape tags in this line. Escape them.
if t.dynamicColors || t.regions {
escapeIndices = escapePattern.FindAllStringIndex(str, -1)
strippedStr = escapePattern.ReplaceAllString(strippedStr, "[$1$2]")
}
// We don't need the original string anymore for now.
str = strippedStr
@@ -810,8 +803,9 @@ func (t *TextView) Draw(screen tcell.Screen) {
colorTags [][]string
escapeIndices [][]int
)
strippedText := text
if t.dynamicColors {
colorTagIndices, colorTags, escapeIndices, _, _ = decomposeString(text)
colorTagIndices, colorTags, escapeIndices, strippedText, _ = decomposeString(text)
}
// Get regions.
@@ -822,8 +816,10 @@ func (t *TextView) Draw(screen tcell.Screen) {
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]"))
}
}
@@ -842,11 +838,26 @@ func (t *TextView) Draw(screen tcell.Screen) {
}
// Print the line.
var currentTag, currentRegion, currentEscapeTag, skipped, runeSeqWidth int
runeSequence := make([]rune, 0, 10)
flush := func() {
if len(runeSequence) == 0 {
return
var colorPos, regionPos, escapePos, tagOffset, skipped int
iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
// Get the color.
if colorPos < len(colorTags) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] {
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[colorPos])
tagOffset += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0]
colorPos++
}
// Get the region.
if regionPos < len(regionIndices) && textPos+tagOffset >= regionIndices[regionPos][0] && textPos+tagOffset < regionIndices[regionPos][1] {
regionID = regions[regionPos][1]
tagOffset += regionIndices[regionPos][1] - regionIndices[regionPos][0]
regionPos++
}
// Skip the second-to-last character of an escape tag.
if escapePos < len(escapeIndices) && textPos+tagOffset == escapeIndices[escapePos][1]-2 {
tagOffset++
escapePos++
}
// Mix the existing style with the new style.
@@ -876,87 +887,30 @@ func (t *TextView) Draw(screen tcell.Screen) {
style = style.Background(fg).Foreground(bg)
}
// Draw the character.
var comb []rune
if len(runeSequence) > 1 && !unicode.IsControl(runeSequence[1]) {
// Allocate space for the combining characters only when necessary.
comb = make([]rune, len(runeSequence)-1)
copy(comb, runeSequence[1:])
}
for offset := 0; offset < runeSeqWidth; offset++ {
screen.SetContent(x+posX+offset, y+line-t.lineOffset, runeSequence[0], comb, style)
}
// Advance.
posX += runeSeqWidth
runeSequence = runeSequence[:0]
runeSeqWidth = 0
}
for pos, ch := range text {
// Get the color.
if currentTag < len(colorTags) && pos >= colorTagIndices[currentTag][0] && pos < colorTagIndices[currentTag][1] {
flush()
if pos == colorTagIndices[currentTag][1]-1 {
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[currentTag])
currentTag++
}
continue
}
// Get the region.
if currentRegion < len(regionIndices) && pos >= regionIndices[currentRegion][0] && pos < regionIndices[currentRegion][1] {
flush()
if pos == regionIndices[currentRegion][1]-1 {
regionID = regions[currentRegion][1]
currentRegion++
}
continue
}
// Skip the second-to-last character of an escape tag.
if currentEscapeTag < len(escapeIndices) && pos >= escapeIndices[currentEscapeTag][0] && pos < escapeIndices[currentEscapeTag][1] {
flush()
if pos == escapeIndices[currentEscapeTag][1]-1 {
currentEscapeTag++
} else if pos == escapeIndices[currentEscapeTag][1]-2 {
continue
}
}
// Determine the width of this rune.
chWidth := runewidth.RuneWidth(ch)
if chWidth == 0 {
// If this is not a modifier, we treat it as a space character.
if len(runeSequence) == 0 {
ch = ' '
chWidth = 1
} else {
runeSequence = append(runeSequence, ch)
continue
}
}
// Skip to the right.
if !t.wrap && skipped < skip {
skipped += chWidth
continue
skipped += screenWidth
return false
}
// Stop at the right border.
if posX+runeSeqWidth+chWidth > width {
break
if posX+screenWidth >= width {
return true
}
// Flush the rune sequence.
flush()
// Draw the character.
for offset := screenWidth - 1; offset >= 0; offset-- {
if offset == 0 {
screen.SetContent(x+posX+offset, y+line-t.lineOffset, main, comb, style)
} else {
screen.SetContent(x+posX+offset, y+line-t.lineOffset, ' ', nil, style)
}
}
// Queue this rune.
runeSequence = append(runeSequence, ch)
runeSeqWidth += chWidth
}
if posX+runeSeqWidth <= width {
flush()
}
// Advance.
posX += screenWidth
return false
})
}
// If this view is not scrollable, we'll purge the buffer of lines that have

546
vendor/github.com/rivo/tview/util.go generated vendored
View File

@@ -1,11 +1,9 @@
package tview
import (
"fmt"
"math"
"regexp"
"strconv"
"strings"
"unicode"
"github.com/gdamore/tcell"
@@ -25,7 +23,7 @@ var (
regionPattern = regexp.MustCompile(`\["([a-zA-Z0-9_,;: \-\.]*)"\]`)
escapePattern = regexp.MustCompile(`\[([a-zA-Z0-9_,;: \-\."#]+)\[(\[*)\]`)
nonEscapePattern = regexp.MustCompile(`(\[[a-zA-Z0-9_,;: \-\."#]+\[*)\]`)
boundaryPattern = regexp.MustCompile("([[:punct:]]\\s*|\\s+)")
boundaryPattern = regexp.MustCompile(`(([[:punct:]]|\n)[ \t\f\r]*|(\s+))`)
spacePattern = regexp.MustCompile(`\s+`)
)
@@ -203,8 +201,8 @@ func decomposeString(text string) (colorIndices [][]int, colors [][]string, esca
// 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.
// 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) {
return printWithStyle(screen, text, x, y, maxWidth, align, tcell.StyleDefault.Foreground(color))
}
@@ -217,115 +215,136 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int,
}
// Decompose the text.
colorIndices, colors, escapeIndices, strippedText, _ := decomposeString(text)
colorIndices, colors, escapeIndices, strippedText, strippedWidth := decomposeString(text)
// We deal with runes, not with bytes.
runes := []rune(strippedText)
// This helper function takes positions for a substring of "runes" and returns
// a new string corresponding to this substring, making sure printing that
// substring will observe color tags.
substring := func(from, to int) string {
// We want to reduce all alignments to AlignLeft.
if align == AlignRight {
if strippedWidth <= maxWidth {
// There's enough space for the entire text.
return printWithStyle(screen, text, x+maxWidth-strippedWidth, y, maxWidth, AlignLeft, style)
}
// Trim characters off the beginning.
var (
colorPos, escapePos, runePos, startPos int
bytes, width, colorPos, escapePos, tagOffset int
foregroundColor, backgroundColor, attributes string
)
if from >= len(runes) {
return ""
}
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 {
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
}
colorPos++
_, originalBackground, _ := style.Decompose()
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(originalBackground, style, 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:]
}
continue
// Print and return.
bytes, width = printWithStyle(screen, text[textPos+tagOffset:], x, y, maxWidth, AlignLeft, style)
return true
}
// Handle escape tags.
if escapePos < len(escapeIndices) && pos >= escapeIndices[escapePos][0] && pos < escapeIndices[escapePos][1] {
if pos == escapeIndices[escapePos][1]-1 {
escapePos++
} else if pos == escapeIndices[escapePos][1]-2 {
continue
}
}
// Check boundaries.
if runePos == from {
startPos = pos
} else if runePos >= to {
return fmt.Sprintf(`[%s:%s:%s]%s`, foregroundColor, backgroundColor, attributes, text[startPos:pos])
}
runePos++
}
return fmt.Sprintf(`[%s:%s:%s]%s`, foregroundColor, backgroundColor, attributes, text[startPos:])
}
// We want to reduce everything to AlignLeft.
if align == AlignRight {
width := 0
start := len(runes)
for index := start - 1; index >= 0; index-- {
w := runewidth.RuneWidth(runes[index])
if width+w > maxWidth {
break
}
width += w
start = index
}
for start < len(runes) && runewidth.RuneWidth(runes[start]) == 0 {
start++
}
return printWithStyle(screen, substring(start, len(runes)), x+maxWidth-width, y, width, AlignLeft, style)
return false
})
return bytes, width
} else if align == AlignCenter {
width := runewidth.StringWidth(strippedText)
if width == maxWidth {
if strippedWidth == maxWidth {
// Use the exact space.
return printWithStyle(screen, text, x, y, maxWidth, AlignLeft, style)
} else if width < maxWidth {
} else if strippedWidth < maxWidth {
// We have more space than we need.
half := (maxWidth - width) / 2
half := (maxWidth - strippedWidth) / 2
return printWithStyle(screen, text, x+half, y, maxWidth-half, AlignLeft, style)
} else {
// Chop off runes until we have a perfect fit.
var choppedLeft, choppedRight, leftIndex, rightIndex int
rightIndex = len(runes) - 1
for rightIndex > leftIndex && width-choppedLeft-choppedRight > maxWidth {
rightIndex = len(strippedText)
for rightIndex-1 > leftIndex && strippedWidth-choppedLeft-choppedRight > maxWidth {
if choppedLeft < choppedRight {
leftWidth := runewidth.RuneWidth(runes[leftIndex])
choppedLeft += leftWidth
leftIndex++
for leftIndex < len(runes) && leftIndex < rightIndex && runewidth.RuneWidth(runes[leftIndex]) == 0 {
leftIndex++
}
// Iterate on the left by one character.
iterateString(strippedText[leftIndex:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
choppedLeft += screenWidth
leftIndex += textWidth
return true
})
} else {
rightWidth := runewidth.RuneWidth(runes[rightIndex])
choppedRight += rightWidth
rightIndex--
// 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
})
}
}
return printWithStyle(screen, substring(leftIndex, rightIndex), x, y, maxWidth, AlignLeft, style)
// Add tag offsets and determine start style.
var (
colorPos, escapePos, tagOffset int
foregroundColor, backgroundColor, attributes string
)
_, originalBackground, _ := style.Decompose()
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(originalBackground, style, 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++
}
}
return printWithStyle(screen, text[leftIndex+tagOffset:], x, y, maxWidth, AlignLeft, style)
}
}
// Draw text.
drawn := 0
drawnWidth := 0
var (
colorPos, escapePos int
foregroundColor, backgroundColor, attributes string
drawn, drawnWidth, colorPos, escapePos, tagOffset int
foregroundColor, backgroundColor, attributes string
)
runeSequence := make([]rune, 0, 10)
runeSeqWidth := 0
flush := func() {
if len(runeSequence) == 0 {
return // Nothing to flush.
iterateString(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
}
// Handle color tags.
if 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 scape tags.
if escapePos < len(escapeIndices) && textPos+tagOffset >= escapeIndices[escapePos][0] && textPos+tagOffset < escapeIndices[escapePos][1] {
if textPos+tagOffset == escapeIndices[escapePos][1]-2 {
tagOffset++
escapePos++
}
}
// Print the rune sequence.
@@ -333,69 +352,23 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int,
_, _, finalStyle, _ := screen.GetContent(finalX, y)
_, background, _ := finalStyle.Decompose()
finalStyle = overlayStyle(background, style, foregroundColor, backgroundColor, attributes)
var comb []rune
if len(runeSequence) > 1 && !unicode.IsControl(runeSequence[1]) {
// Allocate space for the combining characters only when necessary.
comb = make([]rune, len(runeSequence)-1)
copy(comb, runeSequence[1:])
}
for offset := 0; offset < runeSeqWidth; offset++ {
// To avoid undesired effects, we place the same character in all cells.
screen.SetContent(finalX+offset, y, runeSequence[0], comb, finalStyle)
}
// Advance and reset.
drawn += len(runeSequence)
drawnWidth += runeSeqWidth
runeSequence = runeSequence[:0]
runeSeqWidth = 0
}
for pos, ch := range text {
// Handle color tags.
if colorPos < len(colorIndices) && pos >= colorIndices[colorPos][0] && pos < colorIndices[colorPos][1] {
flush()
if pos == colorIndices[colorPos][1]-1 {
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
colorPos++
}
continue
}
// Handle escape tags.
if escapePos < len(escapeIndices) && pos >= escapeIndices[escapePos][0] && pos < escapeIndices[escapePos][1] {
flush()
if pos == escapeIndices[escapePos][1]-1 {
escapePos++
} else if pos == escapeIndices[escapePos][1]-2 {
continue
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)
}
}
// Check if we have enough space for this rune.
chWidth := runewidth.RuneWidth(ch)
if drawnWidth+chWidth > maxWidth {
break // No. We're done then.
}
// Advance.
drawn += length
drawnWidth += screenWidth
// Put this rune in the queue.
if chWidth == 0 {
// If this is not a modifier, we treat it as a space character.
if len(runeSequence) == 0 {
ch = ' '
chWidth = 1
}
} else {
// We have a character. Flush all previous runes.
flush()
}
runeSequence = append(runeSequence, ch)
runeSeqWidth += chWidth
}
if drawnWidth+runeSeqWidth <= maxWidth {
flush()
}
return false
})
return drawn, drawnWidth
return drawn + tagOffset + len(escapeIndices), drawnWidth
}
// PrintSimple prints white text to the screen at the given position.
@@ -421,102 +394,83 @@ func WordWrap(text string, width int) (lines []string) {
colorTagIndices, _, escapeIndices, strippedText, _ := decomposeString(text)
// Find candidate breakpoints.
breakPoints := boundaryPattern.FindAllStringIndex(strippedText, -1)
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.
// This helper function adds a new line to the result slice. The provided
// positions are in stripped index space.
addLine := func(from, to int) {
// Shift indices back to original index space.
var colorTagIndex, escapeIndex int
for colorTagIndex < len(colorTagIndices) && to >= colorTagIndices[colorTagIndex][0] ||
escapeIndex < len(escapeIndices) && to >= escapeIndices[escapeIndex][0] {
past := 0
if colorTagIndex < len(colorTagIndices) {
tagWidth := colorTagIndices[colorTagIndex][1] - colorTagIndices[colorTagIndex][0]
if colorTagIndices[colorTagIndex][0] < from {
from += tagWidth
to += tagWidth
colorTagIndex++
} else if colorTagIndices[colorTagIndex][0] < to {
to += tagWidth
colorTagIndex++
} else {
past++
}
} else {
past++
}
if escapeIndex < len(escapeIndices) {
tagWidth := escapeIndices[escapeIndex][1] - escapeIndices[escapeIndex][0]
if escapeIndices[escapeIndex][0] < from {
from += tagWidth
to += tagWidth
escapeIndex++
} else if escapeIndices[escapeIndex][0] < to {
to += tagWidth
escapeIndex++
} else {
past++
}
} else {
past++
}
if past == 2 {
break // All other indices are beyond the requested string.
// Process stripped text one character at a time.
var (
colorPos, escapePos, breakpointPos, tagOffset int
lastBreakpoint, lastContinuation, currentLineStart int
lineWidth, continuationWidth int
newlineBreakpoint 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:]
}
}
lines = append(lines, text[from:to])
return substr
}
// Determine final breakpoints.
var start, lastEnd, newStart, breakPoint int
for {
// What's our candidate string?
var candidate string
if breakPoint < len(breakPoints) {
candidate = text[start:breakPoints[breakPoint][1]]
} else {
candidate = text[start:]
iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
// Handle colour tags.
if colorPos < len(colorTagIndices) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] {
tagOffset += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0]
colorPos++
}
candidate = strings.TrimRightFunc(candidate, unicode.IsSpace)
if runewidth.StringWidth(candidate) >= width {
// We're past the available width.
if lastEnd > start {
// Use the previous candidate.
addLine(start, lastEnd)
start = newStart
} else {
// We have no previous candidate. Make a hard break.
var lineWidth int
for index, ch := range text {
if index < start {
continue
}
chWidth := runewidth.RuneWidth(ch)
if lineWidth > 0 && lineWidth+chWidth >= width {
addLine(start, index)
start = index
break
}
lineWidth += chWidth
}
}
} else {
// We haven't hit the right border yet.
if breakPoint >= len(breakPoints) {
// It's the last line. We're done.
if len(candidate) > 0 {
addLine(start, len(strippedText))
}
break
} else {
// We have a new candidate.
lastEnd = start + len(candidate)
newStart = breakPoints[breakPoint][1]
breakPoint++
}
// Handle escape tags.
if escapePos < len(escapeIndices) && textPos+tagOffset == escapeIndices[escapePos][1]-2 {
tagOffset++
escapePos++
}
// Check if a break is warranted.
afterContinuation := lastContinuation > 0 && textPos+tagOffset >= lastContinuation
noBreakpoint := lastContinuation == 0
beyondWidth := lineWidth > 0 && lineWidth > width
if beyondWidth && noBreakpoint {
// We need a hard break without a breakpoint.
lines = append(lines, unescape(text[currentLineStart:textPos+tagOffset], currentLineStart))
currentLineStart = textPos + tagOffset
lineWidth = continuationWidth
} else if afterContinuation && (beyondWidth || newlineBreakpoint) {
// Break at last breakpoint or at newline.
lines = append(lines, unescape(text[currentLineStart:lastBreakpoint], currentLineStart))
currentLineStart = lastContinuation
lineWidth = continuationWidth
lastBreakpoint, lastContinuation, newlineBreakpoint = 0, 0, false
}
// Is this a breakpoint?
if breakpointPos < len(breakpoints) && textPos == breakpoints[breakpointPos][0] {
// Yes, it is. Set up breakpoint infos depending on its type.
lastBreakpoint = breakpoints[breakpointPos][0] + tagOffset
lastContinuation = breakpoints[breakpointPos][1] + tagOffset
newlineBreakpoint = main == '\n'
if breakpoints[breakpointPos][6] < 0 && !newlineBreakpoint {
lastBreakpoint++ // Don't skip punctuation.
}
breakpointPos++
}
// Once we hit the continuation point, we start buffering widths.
if textPos+tagOffset < lastContinuation {
continuationWidth = 0
}
lineWidth += screenWidth
continuationWidth += screenWidth
return false
})
// Flush the rest.
if currentLineStart < len(text) {
lines = append(lines, unescape(text[currentLineStart:], currentLineStart))
}
return
@@ -531,3 +485,121 @@ func WordWrap(text string, width int) (lines []string) {
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 (
runes []rune
lastZeroWidthJoiner bool
startIndex int
startPos int
pos int
)
// Helper function which invokes the callback.
flush := func(index int) bool {
var comb []rune
if len(runes) > 1 {
comb = runes[1:]
}
return callback(runes[0], comb, startIndex, index-startIndex, startPos, pos-startPos)
}
for index, r := range text {
if unicode.In(r, unicode.Lm, unicode.M) || r == '\u200d' {
lastZeroWidthJoiner = r == '\u200d'
} else {
// We have a rune that's not a modifier. It could be the beginning of a
// new character.
if !lastZeroWidthJoiner {
if len(runes) > 0 {
// It is. Invoke callback.
if flush(index) {
return true // We're done.
}
// Reset rune store.
runes = runes[:0]
startIndex = index
startPos = pos
}
pos += runewidth.RuneWidth(r)
} else {
lastZeroWidthJoiner = false
}
}
runes = append(runes, r)
}
// Flush any remaining runes.
if len(runes) > 0 {
flush(len(text))
}
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 runePos struct {
r rune
pos int // The byte position of the rune in the original string.
width int // The screen width of the rune.
mod bool // Modifier or zero-width-joiner.
}
// We use the following:
// len(text) >= number of runes in text.
// Put all runes into a runePos slice in reverse.
runesReverse := make([]runePos, len(text))
index := len(text) - 1
for pos, ch := range text {
runesReverse[index].r = ch
runesReverse[index].pos = pos
runesReverse[index].width = runewidth.RuneWidth(ch)
runesReverse[index].mod = unicode.In(ch, unicode.Lm, unicode.M) || ch == '\u200d'
index--
}
runesReverse = runesReverse[index+1:]
// Parse reverse runes.
var screenWidth int
buffer := make([]rune, len(text)) // We fill this up from the back so it's forward again.
bufferPos := len(text)
stringWidth := runewidth.StringWidth(text)
for index, r := range runesReverse {
// Put this rune into the buffer.
bufferPos--
buffer[bufferPos] = r.r
// Do we need to flush the buffer?
if r.pos == 0 || !r.mod && runesReverse[index+1].r != '\u200d' {
// Yes, invoke callback.
var comb []rune
if len(text)-bufferPos > 1 {
comb = buffer[bufferPos+1:]
}
if callback(r.r, comb, r.pos, len(text)-r.pos, stringWidth-screenWidth, r.width) {
return true
}
screenWidth += r.width
bufferPos = len(text)
}
}
return false
}