mirror of
https://github.com/rivo/tview.git
synced 2025-04-24 13:48:56 +08:00
Finished new parser implementation.
This commit is contained in:
parent
caea67a4ef
commit
7344139b55
@ -19,7 +19,7 @@ const logo = `
|
||||
|
||||
const (
|
||||
subtitle = `tview - Rich Widgets for Terminal UIs`
|
||||
navigation = `Ctrl-N: Next slide Ctrl-P: Previous slide Ctrl-C: Exit`
|
||||
navigation = `[yellow]Ctrl-N[-]: Next slide [yellow]Ctrl-P[-]: Previous slide [yellow]Ctrl-C[-]: Exit`
|
||||
mouse = `(or use your mouse)`
|
||||
)
|
||||
|
||||
|
@ -9,10 +9,12 @@ import (
|
||||
|
||||
// End shows the final slide.
|
||||
func End(nextSlide func()) (title string, content tview.Primitive) {
|
||||
textView := tview.NewTextView().SetDoneFunc(func(key tcell.Key) {
|
||||
nextSlide()
|
||||
})
|
||||
url := "https://github.com/rivo/tview"
|
||||
textView := tview.NewTextView().
|
||||
SetDynamicColors(true).
|
||||
SetDoneFunc(func(key tcell.Key) {
|
||||
nextSlide()
|
||||
})
|
||||
url := "[:::https://github.com/rivo/tview]https://github.com/rivo/tview"
|
||||
fmt.Fprint(textView, url)
|
||||
return "End", Center(len(url), 1, textView)
|
||||
return "End", Center(tview.TaggedStringWidth(url), 1, textView)
|
||||
}
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 168 KiB |
40
doc.go
40
doc.go
@ -53,11 +53,6 @@ application, set the box as its root primitive, and run the event loop. The
|
||||
application exits when the application's [Application.Stop] function is called
|
||||
or when Ctrl-C is pressed.
|
||||
|
||||
If we have a primitive which consumes key presses, we call the application's
|
||||
[Application.SetFocus] function to redirect all key presses to that primitive.
|
||||
Most primitives then offer ways to install handlers that allow you to react to
|
||||
any actions performed on them.
|
||||
|
||||
# More Demos
|
||||
|
||||
You will find more demos in the "demos" subdirectory. It also contains a
|
||||
@ -114,8 +109,8 @@ previously set.
|
||||
|
||||
Setting a URL allows you to turn a piece of text into a hyperlink in some
|
||||
terminals. Specify a dash ("-") to specify the end of the hyperlink. Hyperlinks
|
||||
must only contain single-byte characters (e.g. ASCII), excluding bracket
|
||||
characters ("[" or "]").
|
||||
must only contain single-byte characters (e.g. ASCII) and they may not contain
|
||||
bracket characters ("[" or "]").
|
||||
|
||||
Examples:
|
||||
|
||||
@ -128,7 +123,7 @@ Examples:
|
||||
[-]Reset foreground color
|
||||
[::i]Italic and [::I]not italic
|
||||
Click [:::https://example.com]here[:::-] for example.com.
|
||||
Send an email to [:::mailto:her@example.com]her/[:::mail:him@example.com]him[:::-].
|
||||
Send an email to [:::mailto:her@example.com]her/[:::mail:him@example.com]him/[:::mail:them@example.com]them[:::-].
|
||||
[-:-:-:-]Reset everything
|
||||
[:]No effect
|
||||
[]Not a valid style tag, will print square brackets as they are
|
||||
@ -151,18 +146,24 @@ You can use the Escape() function to insert brackets automatically where needed.
|
||||
# Styles
|
||||
|
||||
When primitives are instantiated, they are initialized with colors taken from
|
||||
the global Styles variable. You may change this variable to adapt the look and
|
||||
the global [Styles] variable. You may change this variable to adapt the look and
|
||||
feel of the primitives to your preferred style.
|
||||
|
||||
Note that most terminals will not report information about their color theme.
|
||||
This package therefore does not support using the terminal's color theme. The
|
||||
default style is a dark theme and you must change the [Styles] variable to
|
||||
switch to a light (or other) theme.
|
||||
|
||||
# Unicode Support
|
||||
|
||||
This package supports unicode characters including wide characters.
|
||||
This package supports all unicode characters supported by your terminal.
|
||||
|
||||
# Concurrency
|
||||
|
||||
Many functions in this package are not thread-safe. For many applications, this
|
||||
may not be an issue: If your code makes changes in response to key events, it
|
||||
will execute in the main goroutine and thus will not cause any race conditions.
|
||||
is not an issue: If your code makes changes in response to key events, the
|
||||
corresponding callback function will execute in the main goroutine and thus will
|
||||
not cause any race conditions. (Exceptions to this are documented.)
|
||||
|
||||
If you access your primitives from other goroutines, however, you will need to
|
||||
synchronize execution. The easiest way to do this is to call
|
||||
@ -182,15 +183,17 @@ documentation for details.
|
||||
You can also call [Application.Draw] from any goroutine without having to wrap
|
||||
it in [Application.QueueUpdate]. And, as mentioned above, key event callbacks
|
||||
are executed in the main goroutine and thus should not use
|
||||
[Application.QueueUpdate] as that may lead to deadlocks.
|
||||
[Application.QueueUpdate] as that may lead to deadlocks. It is also not
|
||||
necessary to call [Application.Draw] from such callbacks as it will be called
|
||||
automatically.
|
||||
|
||||
# Type Hierarchy
|
||||
|
||||
All widgets listed above contain the [Box] type. All of [Box]'s functions are
|
||||
therefore available for all widgets, too. Please note that if you are using the
|
||||
functions of [Box] on a subclass, they will return a *Box, not the subclass. So
|
||||
while tview supports method chaining in many places, these chains must be broken
|
||||
when using [Box]'s functions. Example:
|
||||
functions of [Box] on a subclass, they will return a *Box, not the subclass.
|
||||
This is a Golang limitation. So while tview supports method chaining in many
|
||||
places, these chains must be broken when using [Box]'s functions. Example:
|
||||
|
||||
// This will cause "textArea" to be an empty Box.
|
||||
textArea := tview.NewTextArea().
|
||||
@ -207,7 +210,8 @@ You will need to call [Box.SetBorder] separately:
|
||||
|
||||
All widgets also implement the [Primitive] interface.
|
||||
|
||||
The tview package is based on https://github.com/gdamore/tcell. It uses types
|
||||
and constants from that package (e.g. colors and keyboard values).
|
||||
The tview package's rendering is based on version 2 of
|
||||
https://github.com/gdamore/tcell. It uses types and constants from that package
|
||||
(e.g. colors, styles, and keyboard values).
|
||||
*/
|
||||
package tview
|
||||
|
@ -18,6 +18,21 @@ const (
|
||||
AutocompletedClick // The user selected an autocomplete entry by clicking the mouse button on it.
|
||||
)
|
||||
|
||||
// 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
|
||||
)
|
||||
|
||||
// InputField is a one-line box (three lines if there is a title) where the
|
||||
// user can enter text. Use [InputField.SetAcceptanceFunc] to accept or reject
|
||||
// input, [InputField.SetChangedFunc] to listen for changes, and
|
||||
|
99
strings.go
99
strings.go
@ -495,3 +495,102 @@ func parseTag(str string, state *stepState) (length int, style tcell.Style, regi
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// TaggedStringWidth returns the width of the given string needed to print it on
|
||||
// screen. The text may contain style tags which are not counted.
|
||||
func TaggedStringWidth(text string) (width int) {
|
||||
var state *stepState
|
||||
for len(text) > 0 {
|
||||
_, text, state = step(text, state, stepOptionsStyle)
|
||||
width += state.Width()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// WordWrap splits a text such that each resulting line does not exceed the
|
||||
// given screen width. Split points are determined using the algorithm described
|
||||
// in [Unicode Standard Annex #14].
|
||||
//
|
||||
// This function considers style tags to have no width.
|
||||
//
|
||||
// [Unicode Standard Annex #14]: https://www.unicode.org/reports/tr14/
|
||||
func WordWrap(text string, width int) (lines []string) {
|
||||
if width <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
state *stepState
|
||||
lineWidth, lineLength, lastOption, lastOptionWidth int
|
||||
)
|
||||
str := text
|
||||
for len(str) > 0 {
|
||||
// Parse the next character.
|
||||
var c string
|
||||
c, str, state = step(str, state, stepOptionsStyle)
|
||||
cWidth := state.Width()
|
||||
|
||||
// Would it exceed the line width?
|
||||
if lineWidth+cWidth > width {
|
||||
if lastOptionWidth == 0 {
|
||||
// No split point so far. Just split at the current position.
|
||||
lines = append(lines, text[:lineLength])
|
||||
text = text[lineLength:]
|
||||
lineWidth, lineLength, lastOption, lastOptionWidth = 0, 0, 0, 0
|
||||
} else {
|
||||
// Split at the last split point.
|
||||
lines = append(lines, text[:lastOption])
|
||||
text = text[lastOption:]
|
||||
lineWidth -= lastOptionWidth
|
||||
lineLength -= lastOption
|
||||
lastOption, lastOptionWidth = 0, 0
|
||||
}
|
||||
}
|
||||
|
||||
// Move ahead.
|
||||
lineWidth += cWidth
|
||||
lineLength += state.GrossLength()
|
||||
|
||||
// Check for split points.
|
||||
if lineBreak, optional := state.LineBreak(); lineBreak {
|
||||
if optional {
|
||||
// Remember this split point.
|
||||
lastOption = lineLength
|
||||
lastOptionWidth = lineWidth
|
||||
} else if str != "" || c != "" && uniseg.HasTrailingLineBreakInString(c) {
|
||||
// We must split here.
|
||||
lines = append(lines, strings.TrimRight(text[:lineLength], "\n\r"))
|
||||
text = text[lineLength:]
|
||||
lineWidth, lineLength, lastOption, lastOptionWidth = 0, 0, 0, 0
|
||||
}
|
||||
}
|
||||
}
|
||||
lines = append(lines, text)
|
||||
|
||||
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[]")
|
||||
}
|
||||
|
||||
// stripTags strips style tags from the given string. (Region tags are not
|
||||
// stripped.)
|
||||
func stripTags(text string) string {
|
||||
var (
|
||||
str strings.Builder
|
||||
state *stepState
|
||||
)
|
||||
for len(text) > 0 {
|
||||
var c string
|
||||
c, text, state = step(text, state, stepOptionsStyle)
|
||||
str.WriteString(c)
|
||||
}
|
||||
return str.String()
|
||||
}
|
||||
|
1259
textview.go
1259
textview.go
File diff suppressed because it is too large
Load Diff
350
util.go
350
util.go
@ -4,12 +4,9 @@ import (
|
||||
"math"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/uniseg"
|
||||
)
|
||||
|
||||
// Text alignment within a box. Also used to align images.
|
||||
@ -21,39 +18,12 @@ const (
|
||||
AlignBottom = 2
|
||||
)
|
||||
|
||||
// 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_,;: \-\."#]+)\[(\[*)\]`)
|
||||
// Regular expression used to escape style/region tags.
|
||||
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
|
||||
)
|
||||
|
||||
// The number of colors available in the terminal.
|
||||
var availableColors = 256
|
||||
|
||||
// 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
|
||||
// The number of colors available in the terminal.
|
||||
availableColors = 256
|
||||
)
|
||||
|
||||
// Package initialization.
|
||||
@ -86,153 +56,6 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 style tags (as returned by
|
||||
// re.FindAllStringIndex()), the style 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 style 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, uniseg.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 = uniseg.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.
|
||||
@ -324,24 +147,26 @@ func printWithStyle(screen tcell.Screen, text string, x, y, skipWidth, maxWidth,
|
||||
if c == "" {
|
||||
break // We don't care about the style at the end.
|
||||
}
|
||||
runes := []rune(c)
|
||||
width := state.Width()
|
||||
|
||||
finalStyle := state.Style()
|
||||
if maintainBackground {
|
||||
_, backgroundColor, _ := finalStyle.Decompose()
|
||||
if backgroundColor == tcell.ColorDefault {
|
||||
_, _, existingStyle, _ := screen.GetContent(x, y)
|
||||
_, background, _ := existingStyle.Decompose()
|
||||
finalStyle = finalStyle.Background(background)
|
||||
if width > 0 {
|
||||
finalStyle := state.Style()
|
||||
if maintainBackground {
|
||||
_, backgroundColor, _ := finalStyle.Decompose()
|
||||
if backgroundColor == tcell.ColorDefault {
|
||||
_, _, existingStyle, _ := screen.GetContent(x, y)
|
||||
_, background, _ := existingStyle.Decompose()
|
||||
finalStyle = finalStyle.Background(background)
|
||||
}
|
||||
}
|
||||
}
|
||||
for offset := width - 1; offset >= 0; offset-- {
|
||||
// To avoid undesired effects, we populate all cells.
|
||||
if offset == 0 {
|
||||
screen.SetContent(x+offset, y, runes[0], runes[1:], finalStyle)
|
||||
} else {
|
||||
screen.SetContent(x+offset, y, ' ', nil, finalStyle)
|
||||
for offset := width - 1; offset >= 0; offset-- {
|
||||
// To avoid undesired effects, we populate all cells.
|
||||
runes := []rune(c)
|
||||
if offset == 0 {
|
||||
screen.SetContent(x+offset, y, runes[0], runes[1:], finalStyle)
|
||||
} else {
|
||||
screen.SetContent(x+offset, y, ' ', nil, finalStyle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -357,138 +182,3 @@ func printWithStyle(screen tcell.Screen, text string, x, y, skipWidth, maxWidth,
|
||||
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 style tags which are not counted.
|
||||
func TaggedStringWidth(text string) (width int) {
|
||||
var state *stepState
|
||||
for len(text) > 0 {
|
||||
_, text, state = step(text, state, stepOptionsStyle)
|
||||
width += state.Width()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// WordWrap splits a text such that each resulting line does not exceed the
|
||||
// given screen width. Split points are determined using the algorithm described
|
||||
// in [Unicode Standard Annex #14] .
|
||||
//
|
||||
// This function considers style tags to have no width.
|
||||
//
|
||||
// [Unicode Standard Annex #14]: https://www.unicode.org/reports/tr14/
|
||||
func WordWrap(text string, width int) (lines []string) {
|
||||
if width <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
state *stepState
|
||||
lineWidth, lineLength, lastOption, lastOptionWidth int
|
||||
)
|
||||
str := text
|
||||
for len(str) > 0 {
|
||||
// Parse the next character.
|
||||
var c string
|
||||
c, str, state = step(str, state, stepOptionsStyle)
|
||||
cWidth := state.Width()
|
||||
|
||||
// Would it exceed the line width?
|
||||
if lineWidth+cWidth > width {
|
||||
if lastOptionWidth == 0 {
|
||||
// No split point so far. Just split at the current position.
|
||||
lines = append(lines, text[:lineLength])
|
||||
text = text[lineLength:]
|
||||
lineWidth, lineLength, lastOption, lastOptionWidth = 0, 0, 0, 0
|
||||
} else {
|
||||
// Split at the last split point.
|
||||
lines = append(lines, text[:lastOption])
|
||||
text = text[lastOption:]
|
||||
lineWidth -= lastOptionWidth
|
||||
lineLength -= lastOption
|
||||
lastOption, lastOptionWidth = 0, 0
|
||||
}
|
||||
}
|
||||
|
||||
// Move ahead.
|
||||
lineWidth += cWidth
|
||||
lineLength += state.GrossLength()
|
||||
|
||||
// Check for split points.
|
||||
if lineBreak, optional := state.LineBreak(); lineBreak {
|
||||
if optional {
|
||||
// Remember this split point.
|
||||
lastOption = lineLength
|
||||
lastOptionWidth = lineWidth
|
||||
} else if str != "" || c != "" && uniseg.HasTrailingLineBreakInString(c) {
|
||||
// We must split here.
|
||||
lines = append(lines, strings.TrimRight(text[:lineLength], "\n\r"))
|
||||
text = text[lineLength:]
|
||||
lineWidth, lineLength, lastOption, lastOptionWidth = 0, 0, 0, 0
|
||||
}
|
||||
}
|
||||
}
|
||||
lines = append(lines, text)
|
||||
|
||||
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, the screen width of it, and a boundaries value which includes
|
||||
// word/sentence boundary or line break information (see the
|
||||
// github.com/rivo/uniseg package, Step() function, for more information). 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, boundaries int) bool) bool {
|
||||
var screenPos, textPos, boundaries int
|
||||
|
||||
state := -1
|
||||
for len(text) > 0 {
|
||||
var cluster string
|
||||
cluster, text, boundaries, state = uniseg.StepString(text, state)
|
||||
|
||||
width := boundaries >> uniseg.ShiftWidth
|
||||
runes := []rune(cluster)
|
||||
var comb []rune
|
||||
if len(runes) > 1 {
|
||||
comb = runes[1:]
|
||||
}
|
||||
|
||||
if callback(runes[0], comb, textPos, len(cluster), screenPos, width, boundaries) {
|
||||
return true
|
||||
}
|
||||
|
||||
screenPos += width
|
||||
textPos += len(cluster)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// stripTags strips style tags from the given string. (Region tags are not
|
||||
// stripped.)
|
||||
func stripTags(text string) string {
|
||||
var (
|
||||
str strings.Builder
|
||||
state *stepState
|
||||
)
|
||||
for len(text) > 0 {
|
||||
var c string
|
||||
c, text, state = step(text, state, stepOptionsStyle)
|
||||
str.WriteString(c)
|
||||
}
|
||||
return str.String()
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user