package cviewimport ()// TrueColorTags is a flag which controls whether color tags should render as// the specific colors defined by tcell, or as the colors defined by the user's// terminal configuration (the default).varTrueColorTags = false// ColorUnset represents an unset color. This is necessary because the zero// value of color, ColorDefault, results in default terminal colors.varColorUnset = tcell.ColorSpecial | 108// Horizontal alignment within a box.const (AlignLeft = iotaAlignCenterAlignRight)// VerticalAlignment represents vertical alignment.typeVerticalAlignmentint// Vertical alignment within a box.const (AlignTopVerticalAlignment = iotaAlignMiddleAlignBottom)// Common regular expressions.var ( colorPattern = regexp.MustCompile(`\[([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([bdilrsu]+|\-)?)?)?\]`) regionPattern = regexp.MustCompile(`\["([a-zA-Z0-9_,;: \-\.]*)"\]`) escapePattern = regexp.MustCompile(`\[([a-zA-Z0-9_,;: \-\."#]+)\[(\[*)\]`) nonEscapePattern = regexp.MustCompile(`(\[[a-zA-Z0-9_,;: \-\."#]+\[*)\]`) boundaryPattern = regexp.MustCompile(`(([,\.\-:;!\?&#+]|\n)[ \t\f\r]*|([ \t\f\r]+))`) spacePattern = regexp.MustCompile(`\s+`))// Positions of substrings in regular expressions.const ( colorForegroundPos = 1 colorBackgroundPos = 3 colorFlagPos = 5)// Predefined InputField acceptance functions.var (// InputFieldInteger accepts integers.InputFieldIntegerfunc(text string, ch rune) bool// InputFieldFloat accepts floating-point numbers.InputFieldFloatfunc(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.InputFieldMaxLengthfunc(maxLength int) func(text string, ch rune) bool)// Transformation describes a widget state modification.typeTransformationint// Widget transformations.const (TransformFirstItemTransformation = 1TransformLastItemTransformation = 2TransformPreviousItemTransformation = 3TransformNextItemTransformation = 4TransformPreviousPageTransformation = 5TransformNextPageTransformation = 6)// Package initialization.func init() {runewidth.EastAsianWidth = truerunewidth.CreateLUT() // Create lookup table// Initialize the predefined input field handlers.InputFieldInteger = func( string, rune) bool {if == "-" {returntrue } , := strconv.Atoi()return == nil }InputFieldFloat = func( string, rune) bool {if == "-" || == "." || == "-." {returntrue } , := strconv.ParseFloat(, 64)return == nil }InputFieldMaxLength = func( int) func( string, rune) bool {returnfunc( string, rune) bool {returnlen([]rune()) <= } }}// StripTags returns the provided text without color and/or region tags.func ( []byte, bool, bool) []byte {if ! && ! { := make([]byte, len())copy(, )return }var []byte := if { = regionPattern.ReplaceAll(, nil) = }if { = colorPattern.ReplaceAllFunc(, func( []byte) []byte {iflen() > 2 {returnnil }return }) }returnescapePattern.ReplaceAll(, []byte(`[$1$2]`))}// ColorHex returns the hexadecimal value of a color as a string, prefixed with #.// If the color is invalid, a blank string is returned.func ( tcell.Color) string {if !.Valid() {return"" } , , := .RGB()returnfmt.Sprintf("#%02x%02x%02x", , , )}// 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(, , string, [][]byte) (, , string) {iflen([colorForegroundPos]) > 0 { := string([colorForegroundPos])if == "-" { = "-" } elseif != "" { = } }iflen([colorBackgroundPos-1]) > 0 { := string([colorBackgroundPos])if == "-" { = "-" } elseif != "" { = } }iflen([colorFlagPos-1]) > 0 { := string([colorFlagPos])if == "-" { = "-" } elseif != "" { = } }return , , }// overlayStyle mixes a background color with a foreground color (fgColor),// a (possibly new) background color (bgColor), and style attributes, and// returns the resulting style. For a definition of the colors and attributes,// see styleFromTag(). Reset instructions cause the corresponding part of the// default style to be used.func overlayStyle( tcell.Color, tcell.Style, , , string) tcell.Style { , , := .Decompose() := .Background() = .Foreground()if != "" {if == "-" { = .Foreground() } else { := tcell.GetColor()ifTrueColorTags { = .TrueColor() } = .Foreground() } }if == "-" || == "" && != tcell.ColorDefault { = .Background() } elseif != "" { := tcell.GetColor()ifTrueColorTags { = .TrueColor() } = .Background() }if == "-" { = .Bold(&tcell.AttrBold > 0) = .Dim(&tcell.AttrDim > 0) = .Italic(&tcell.AttrItalic > 0) = .Blink(&tcell.AttrBlink > 0) = .Reverse(&tcell.AttrReverse > 0) = .StrikeThrough(&tcell.AttrStrikeThrough > 0) = .Underline(&tcell.AttrUnderline > 0) } elseif != "" { = .Normal()for , := range {switch {case'b': = .Bold(true)case'd': = .Dim(true)case'i': = .Italic(true)case'l': = .Blink(true)case'r': = .Reverse(true)case's': = .StrikeThrough(true)case'u': = .Underline(true) } } }return}// SetAttributes sets attributes on a style.func ( tcell.Style, tcell.AttrMask) tcell.Style {return .Bold(&tcell.AttrBold != 0).Dim(&tcell.AttrDim != 0).Italic(&tcell.AttrItalic != 0).Blink(&tcell.AttrBlink != 0).Reverse(&tcell.AttrReverse != 0).StrikeThrough(&tcell.AttrStrikeThrough != 0).Underline(&tcell.AttrUnderline != 0)}// decomposeText returns information about a string which may contain color// tags or region tags, depending on which ones are requested to be found. It// returns the indices of the color tags (as returned by// re.FindAllStringIndex()), the color tags themselves (as returned by// re.FindAllStringSubmatch()), the indices of region tags and the region tags// themselves, the indices of an escaped tags (only if at least color tags or// region tags are requested), the string stripped by any tags and escaped, and// the screen width of the stripped string.func decomposeText( []byte, , bool) ( [][]int, [][][]byte, [][]int, [][][]byte, [][]int, []byte, int) {// Shortcut for the trivial case.if ! && ! {returnnil, nil, nil, nil, nil, , runewidth.StringWidth(string()) }// Get positions of any tags.if { = colorPattern.FindAllIndex(, -1) = colorPattern.FindAllSubmatch(, -1) }if { = regionPattern.FindAllIndex(, -1) = regionPattern.FindAllSubmatch(, -1) } = escapePattern.FindAllIndex(, -1)// Because the color pattern detects empty tags, we need to filter them out.for := len() - 1; >= 0; -- {if [][1]-[][0] == 2 { = append([:], [+1:]...) = append([:], [+1:]...) } }// Make a (sorted) list of all tags.var [][]intif && { = = make([][]int, len()+len())copy(, )copy([len():], )sort.Slice(, func( int, int) bool {return [][0] < [][0] }) } elseif { = } else { = }// Remove the tags from the original string.varint := make([]byte, 0, len())for , := range { = append(, [:[0]]...) = [1] } = append(, [:]...)// Escape string. = escapePattern.ReplaceAll(, []byte("[$1$2]"))// Get the width of the stripped string. = runewidth.StringWidth(string())return}// Print prints text onto the screen into the given box at (x,y,maxWidth,1),// not exceeding that box. "align" is one of AlignLeft, AlignCenter, or// AlignRight. The screen's background color will not be changed.//// You can change the colors and text styles mid-text by inserting a color tag.// See the package description for details.//// Returns the number of actual bytes of the text printed (including color tags)// and the actual width used for the printed runes.func ( tcell.Screen, []byte, , , , int, tcell.Color) (int, int) {returnPrintStyle(, , , , , , tcell.StyleDefault.Foreground())}// PrintStyle works like Print() but it takes a style instead of just a// foreground color.func ( tcell.Screen, []byte, , , , int, tcell.Style) (int, int) {if <= 0 || len() == 0 {return0, 0 }// Decompose the text. , , , , , , := decomposeText(, true, false)// We want to reduce all alignments to AlignLeft.if == AlignRight {if <= {// There's enough space for the entire text.return (, , +-, , , AlignLeft, ) }// Trim characters off the beginning.var ( , , , , int , , string ) , , := .Decompose()iterateString(string(), func( rune, []rune, , , , int) bool {// Update color/escape tag offset and style.if < len() && + >= [][0] && + < [][1] { , , = styleFromTag(, , , []) = overlayStyle(, , , , ) += [][1] - [][0] ++ }if < len() && + >= [][0] && + < [][1] { ++ ++ }if - < {// We chopped off enough.if > 0 && +-1 >= [-1][0] && +-1 < [-1][1] {// Unescape open escape sequences. := [-1][1] - 2 = append([:], [+1:]...) }// Print and return. , = (, [+:], , , , AlignLeft, )returntrue }returnfalse })return , } elseif == AlignCenter {if == {// Use the exact space.return (, , , , , AlignLeft, ) } elseif < {// We have more space than we need. := ( - ) / 2return (, , +, , -, AlignLeft, ) } else {// Chop off runes until we have a perfect fit.var , , , int = len()for -1 > && -- > {if < {// Iterate on the left by one character.iterateString(string([:]), func( rune, []rune, , , , int) bool { += += returntrue }) } else {// Iterate on the right by one character.iterateStringReverse(string([:]), func( rune, []rune, , , , int) bool { += -= returntrue }) } }// Add tag offsets and determine start style.var ( , , int , , string ) , , := .Decompose()for := range {// We only need the offset of the left index.if > {// We're done.if > 0 && +-1 >= [-1][0] && +-1 < [-1][1] {// Unescape open escape sequences. := [-1][1] - 2 = append([:], [+1:]...) }break }// Update color/escape tag offset.if < len() && + >= [][0] && + < [][1] {if <= { , , = styleFromTag(, , , []) = overlayStyle(, , , , ) } += [][1] - [][0] ++ }if < len() && + >= [][0] && + < [][1] { ++ ++ } }return (, [+:], , , , AlignLeft, ) } }// Draw text.var ( , , , , int , , string )iterateString(string(), func( rune, []rune, , , , int) bool {// Only continue if there is still space.if + > {returntrue }// Handle color tags.for < len() && + >= [][0] && + < [][1] { , , = styleFromTag(, , , []) += [][1] - [][0] ++ }// Handle scape tags.if < len() && + >= [][0] && + < [][1] {if + == [][1]-2 { ++ ++ } }// Print the rune sequence. := + , , , := .GetContent(, ) , , := .Decompose() = overlayStyle(, , , , )for := - 1; >= 0; -- {// To avoid undesired effects, we populate all cells.if == 0 { .SetContent(+, , , , ) } else { .SetContent(+, , ' ', nil, ) } }// Advance. += += returnfalse })return + + len(), }// PrintSimple prints white text to the screen at the given position.func ( tcell.Screen, []byte, , int) {Print(, , , , math.MaxInt32, AlignLeft, Styles.PrimaryTextColor)}// TaggedTextWidth returns the width of the given string needed to print it on// screen. The text may contain color tags which are not counted.func ( []byte) int { , , , , , , := decomposeText(, true, false)return}// TaggedStringWidth returns the width of the given string needed to print it on// screen. The text may contain color tags which are not counted.func ( string) int {returnTaggedTextWidth([]byte())}// WordWrap splits a text such that each resulting line does not exceed the// given screen width. Possible split points are after any punctuation or// whitespace. Whitespace after split points will be dropped.//// This function considers color tags to have no width.//// Text is always split at newline characters ('\n').//// BUG(tslocum) Text containing square brackets is not escaped properly.// Use TextView.SetWrapWidth where possible.//// Issue: https://code.rocketnine.space/tslocum/cview/issues/27func ( string, int) ( []string) { , , , , , , := decomposeText([]byte(), true, false)// Find candidate breakpoints. := boundaryPattern.FindAllSubmatchIndex(, -1)// Results in one entry for each candidate. Each entry is an array a of // indices into strippedText where a[6] < 0 for newline/punctuation matches // and a[4] < 0 for whitespace matches.// Process stripped text one character at a time.var ( , , , int , , int , intbool ) := func( string, int) string {// A helper function to unescape escaped tags.for := ; >= 0; -- {if < len() && > [][0] && < [][1]-1 { := [][1] - 2 - if < 0 || > len() { // Workaround for issue #27return }return [:] + [+1:] } }return }iterateString(string(), func( rune, []rune, , , , int) bool {// Handle tags.for {if < len() && + >= [][0] && + < [][1] {// Colour tags. += [][1] - [][0] ++ } elseif < len() && + == [][1]-2 {// Escape tags. ++ ++ } else {break } }// Is this a breakpoint?if < len() && + == [][0] {// Yes, it is. Set up breakpoint infos depending on its type. = [][0] + = [][1] + = 0 = == '\n'if [][6] < 0 && ! { ++ // Don't skip punctuation. } ++ }// Check if a break is warranted.if || > 0 && + > { := := if { = + = + + 1 = 0 = 0 } elseif <= { = + = + = 0 } = append(, ([:], )) , , = , , false }// Remember the characters since the last breakpoint.if > 0 && <= + { += }// Advance. += // But if we're still inside a breakpoint, skip next character (whitespace).if + < { -= }returnfalse })// Flush the rest.if < len() { = append(, ([:], )) }return}// EscapeBytes 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(cview.Escape("[squarebrackets]"))// fmt.Fprint(textView, cview.EscapeBytes(`["quoted"]`))func ( []byte) []byte {returnnonEscapePattern.ReplaceAll(, []byte("$1[]"))}// 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(cview.Escape("[squarebrackets]"))// fmt.Fprint(textView, cview.Escape(`["quoted"]`))func ( string) string {returnnonEscapePattern.ReplaceAllString(, "$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( string, func( rune, []rune, , , , int) bool) bool {varint := uniseg.NewGraphemes()for .Next() { := .Runes() , := .Positions() := runewidth.StringWidth(.Str())var []runeiflen() > 1 { = [1:] }if ([0], , , -, , ) {returntrue } += }returnfalse}// 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( string, func( rune, []rune, , , , int) bool) bool {typestruct {rune []rune , , , int }// Create the grapheme clusters.var []iterateString(, func( rune, []rune, int, int, int, int) bool { = append(, { : , : , : , : , : , : , })returnfalse })// Iterate in reverse.for := len() - 1; >= 0; -- {if ( []., []., []., []., []., []., ) {returntrue } }returnfalse}// ScrollBarVisibility specifies the display of a scroll bar.typeScrollBarVisibilityintconst (// ScrollBarNever never shows a scroll bar.ScrollBarNeverScrollBarVisibility = iota// ScrollBarAuto shows a scroll bar when there are items offscreen.ScrollBarAuto// ScrollBarAlways always shows a scroll bar.ScrollBarAlways)// Scroll bar render text (must be one cell wide)var (ScrollBarArea = []byte("[-:-:-]░")ScrollBarAreaFocused = []byte("[-:-:-]▒")ScrollBarHandle = []byte("[-:-:-]▓")ScrollBarHandleFocused = []byte("[::r] [-:-:-]"))// RenderScrollBar renders a scroll bar at the specified position.func ( tcell.Screen, ScrollBarVisibility, int, int, int, int, int, int, bool, tcell.Color) {if == ScrollBarNever || ( == ScrollBarAuto && <= ) {return }// Place cursor at top when there are no items offscreen.if <= { = 0 }// Handle negative cursor.if < 0 { = 0 }// Calculate handle position. := int(float64(-1) * (float64() / float64(-1)))// Print scroll bar.var []byteif == {if { = ScrollBarHandleFocused } else { = ScrollBarHandle } } else {if { = ScrollBarAreaFocused } else { = ScrollBarArea } }Print(, , , , 1, AlignLeft, )}
The pages are generated with Goldsv0.8.2. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.