Finished new parser implementation.

pull/873/head
Oliver 9 months ago
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

@ -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

@ -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()
}

File diff suppressed because it is too large Load Diff

@ -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…
Cancel
Save