diff --git a/Gopkg.lock b/Gopkg.lock index ba6ad61..b78c6ef 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -139,7 +139,7 @@ branch = "master" name = "maunium.net/go/tview" packages = ["."] - revision = "6146b7fe2331e23a78e217016705bc6801bfc55a" + revision = "7eabba90a261a481d36ace89daa79c56582238d7" [solve-meta] analyzer-name = "dep" diff --git a/vendor/maunium.net/go/tview/README.md b/vendor/maunium.net/go/tview/README.md index fb99073..3e5734e 100644 --- a/vendor/maunium.net/go/tview/README.md +++ b/vendor/maunium.net/go/tview/README.md @@ -64,6 +64,11 @@ 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.14 (2018-04-13) + - Added an `Escape()` function which keep strings like color or region tags from being recognized as such. + - Added `ANSIIWriter()` and `TranslateANSII()` which convert ANSII escape sequences to `tview` color tags. +- v0.13 (2018-04-01) + - Added background colors and text attributes to color tags. - v0.12 (2018-03-13) - Added "suspended mode" to `Application`. - v0.11 (2018-03-02) diff --git a/vendor/maunium.net/go/tview/ansii.go b/vendor/maunium.net/go/tview/ansii.go new file mode 100644 index 0000000..0ce3d4a --- /dev/null +++ b/vendor/maunium.net/go/tview/ansii.go @@ -0,0 +1,237 @@ +package tview + +import ( + "bytes" + "fmt" + "io" + "strconv" + "strings" +) + +// The states of the ANSII escape code parser. +const ( + ansiiText = iota + ansiiEscape + ansiiSubstring + ansiiControlSequence +) + +// ansii is a io.Writer which translates ANSII escape codes into tview color +// tags. +type ansii struct { + io.Writer + + // Reusable buffers. + buffer *bytes.Buffer // The entire output text of one Write(). + csiParameter, csiIntermediate *bytes.Buffer // Partial CSI strings. + + // The current state of the parser. One of the ansii constants. + state int +} + +// ANSIIWriter returns an io.Writer which translates any ANSII escape codes +// written to it into tview color tags. Other escape codes don't have an effect +// and are simply removed. The translated text is written to the provided +// writer. +func ANSIIWriter(writer io.Writer) io.Writer { + return &ansii{ + Writer: writer, + buffer: new(bytes.Buffer), + csiParameter: new(bytes.Buffer), + csiIntermediate: new(bytes.Buffer), + state: ansiiText, + } +} + +// Write parses the given text as a string of runes, translates ANSII escape +// codes to color tags and writes them to the output writer. +func (a *ansii) Write(text []byte) (int, error) { + defer func() { + a.buffer.Reset() + }() + + for _, r := range string(text) { + switch a.state { + + // We just entered an escape sequence. + case ansiiEscape: + switch r { + case '[': // Control Sequence Introducer. + a.csiParameter.Reset() + a.csiIntermediate.Reset() + a.state = ansiiControlSequence + case 'c': // Reset. + fmt.Fprint(a.buffer, "[-:-:-]") + a.state = ansiiText + case 'P', ']', 'X', '^', '_': // Substrings and commands. + a.state = ansiiSubstring + default: // Ignore. + a.state = ansiiText + } + + // CSI Sequences. + case ansiiControlSequence: + switch { + case r >= 0x30 && r <= 0x3f: // Parameter bytes. + if _, err := a.csiParameter.WriteRune(r); err != nil { + return 0, err + } + case r >= 0x20 && r <= 0x2f: // Intermediate bytes. + if _, err := a.csiIntermediate.WriteRune(r); err != nil { + return 0, err + } + case r >= 0x40 && r <= 0x7e: // Final byte. + switch r { + case 'E': // Next line. + count, _ := strconv.Atoi(a.csiParameter.String()) + if count == 0 { + count = 1 + } + fmt.Fprint(a.buffer, strings.Repeat("\n", count)) + case 'm': // Select Graphic Rendition. + var ( + background, foreground, attributes string + clearAttributes bool + ) + fields := strings.Split(a.csiParameter.String(), ";") + if len(fields) == 0 || len(fields) == 1 && fields[0] == "0" { + // Reset. + if _, err := a.buffer.WriteString("[-:-:-]"); err != nil { + return 0, err + } + break + } + lookupColor := func(colorNumber int, bright bool) string { + if colorNumber < 0 || colorNumber > 7 { + return "black" + } + if bright { + colorNumber += 8 + } + return [...]string{ + "black", + "red", + "green", + "yellow", + "blue", + "darkmagenta", + "darkcyan", + "white", + "#7f7f7f", + "#ff0000", + "#00ff00", + "#ffff00", + "#5c5cff", + "#ff00ff", + "#00ffff", + "#ffffff", + }[colorNumber] + } + for index, field := range fields { + switch field { + case "1", "01": + attributes += "b" + case "2", "02": + attributes += "d" + case "4", "04": + attributes += "u" + case "5", "05": + attributes += "l" + case "7", "07": + attributes += "7" + case "22", "24", "25", "27": + clearAttributes = true + case "30", "31", "32", "33", "34", "35", "36", "37": + colorNumber, _ := strconv.Atoi(field) + foreground = lookupColor(colorNumber-30, false) + case "40", "41", "42", "43", "44", "45", "46", "47": + colorNumber, _ := strconv.Atoi(field) + background = lookupColor(colorNumber-40, false) + case "90", "91", "92", "93", "94", "95", "96", "97": + colorNumber, _ := strconv.Atoi(field) + foreground = lookupColor(colorNumber-90, true) + case "100", "101", "102", "103", "104", "105", "106", "107": + colorNumber, _ := strconv.Atoi(field) + background = lookupColor(colorNumber-100, true) + case "38", "48": + var color string + if len(fields) > index+1 { + if fields[index+1] == "5" && len(fields) > index+2 { // 8-bit colors. + colorNumber, _ := strconv.Atoi(fields[index+2]) + if colorNumber <= 7 { + color = lookupColor(colorNumber, false) + } else if colorNumber <= 15 { + color = lookupColor(colorNumber, true) + } else if colorNumber <= 231 { + red := (colorNumber - 16) / 36 + green := ((colorNumber - 16) / 6) % 6 + blue := (colorNumber - 16) % 6 + color = fmt.Sprintf("%02x%02x%02x", 255*red/5, 255*green/5, 255*blue/5) + } else if colorNumber <= 255 { + grey := 255 * (colorNumber - 232) / 23 + color = fmt.Sprintf("%02x%02x%02x", grey, grey, grey) + } + } else if fields[index+1] == "2" && len(fields) > index+4 { // 24-bit colors. + red, _ := strconv.Atoi(fields[index+2]) + green, _ := strconv.Atoi(fields[index+3]) + blue, _ := strconv.Atoi(fields[index+4]) + color = fmt.Sprintf("%02x%02x%02x", red, green, blue) + } + } + if len(color) > 0 { + if field == "38" { + foreground = color + } else { + background = color + } + } + } + } + if len(attributes) > 0 || clearAttributes { + attributes = ":" + attributes + } + if len(foreground) > 0 || len(background) > 0 || len(attributes) > 0 { + fmt.Fprintf(a.buffer, "[%s:%s%s]", foreground, background, attributes) + } + } + a.state = ansiiText + default: // Undefined byte. + a.state = ansiiText // Abort CSI. + } + + // We just entered a substring/command sequence. + case ansiiSubstring: + if r == 27 { // Most likely the end of the substring. + a.state = ansiiEscape + } // Ignore all other characters. + + // "ansiiText" and all others. + default: + if r == 27 { + // This is the start of an escape sequence. + a.state = ansiiEscape + } else { + // Just a regular rune. Send to buffer. + if _, err := a.buffer.WriteRune(r); err != nil { + return 0, err + } + } + } + } + + // Write buffer to target writer. + n, err := a.buffer.WriteTo(a.Writer) + if err != nil { + return int(n), err + } + return len(text), nil +} + +// TranslateANSII replaces ANSII escape sequences found in the provided string +// with tview's color tags and returns the resulting string. +func TranslateANSII(text string) string { + var buffer bytes.Buffer + writer := ANSIIWriter(&buffer) + writer.Write([]byte(text)) + return buffer.String() +} diff --git a/vendor/maunium.net/go/tview/application.go b/vendor/maunium.net/go/tview/application.go index f3d6328..387c4c4 100644 --- a/vendor/maunium.net/go/tview/application.go +++ b/vendor/maunium.net/go/tview/application.go @@ -38,6 +38,8 @@ type Application struct { // be forwarded). mouseCapture func(event *tcell.EventMouse) *tcell.EventMouse + pasteCapture func(event *tcell.EventPaste) *tcell.EventPaste + // An optional callback function which is invoked just before the root // primitive is drawn. beforeDraw func(screen tcell.Screen) bool @@ -190,6 +192,24 @@ func (a *Application) Run() error { //a.Draw() } } + case *tcell.EventPaste: + a.RLock() + p := a.focus + a.RUnlock() + + if a.pasteCapture != nil { + event = a.pasteCapture(event) + if event == nil { + break + } + } + + if p != nil { + if handler := p.PasteHandler(); handler != nil { + handler(event) + a.Draw() + } + } case *tcell.EventResize: a.Lock() screen := a.screen diff --git a/vendor/maunium.net/go/tview/box.go b/vendor/maunium.net/go/tview/box.go index 1bcbff0..ff3cc1e 100644 --- a/vendor/maunium.net/go/tview/box.go +++ b/vendor/maunium.net/go/tview/box.go @@ -62,6 +62,8 @@ type Box struct { // nothing should be forwarded). mouseCapture func(event *tcell.EventMouse) *tcell.EventMouse + pasteCapture func(event *tcell.EventPaste) *tcell.EventPaste + // An optional function which is called before the box is drawn. draw func(screen tcell.Screen, x, y, width, height int) (int, int, int, int) } @@ -218,6 +220,36 @@ func (b *Box) GetMouseCapture() func(event *tcell.EventMouse) *tcell.EventMouse return b.mouseCapture } +func (b *Box) WrapPasteHandler(pasteHandler func(*tcell.EventPaste)) func(*tcell.EventPaste) { + return func(event *tcell.EventPaste) { + if b.pasteCapture != nil { + event = b.pasteCapture(event) + } + if event != nil && pasteHandler != nil { + pasteHandler(event) + } + } +} + +func (b *Box) PasteHandler() func(event *tcell.EventPaste) { + return b.WrapPasteHandler(func(event *tcell.EventPaste) { + // Default paste handler just calls input handler with each character. + inputHandler := b.InputHandler() + for _, char := range event.Text() { + inputHandler(tcell.NewEventKey(tcell.KeyRune, char, tcell.ModNone), nil) + } + }) +} + +func (b *Box) SetPasteCapture(capture func(event *tcell.EventPaste) *tcell.EventPaste) *Box { + b.pasteCapture = capture + return b +} + +func (b *Box) GetPasteCapture() func(event *tcell.EventPaste) *tcell.EventPaste { + return b.pasteCapture +} + // SetBackgroundColor sets the box's background color. func (b *Box) SetBackgroundColor(color tcell.Color) *Box { b.backgroundColor = color diff --git a/vendor/maunium.net/go/tview/checkbox.go b/vendor/maunium.net/go/tview/checkbox.go index 83404b8..ae58720 100644 --- a/vendor/maunium.net/go/tview/checkbox.go +++ b/vendor/maunium.net/go/tview/checkbox.go @@ -17,6 +17,10 @@ type Checkbox struct { // The text to be displayed before the input area. label string + // The screen width of the label area. A value of 0 means use the width of + // the label text. + labelWidth int + // The label color. labelColor tcell.Color @@ -34,6 +38,10 @@ type Checkbox struct { // are done entering text. The key which was pressed is provided (tab, // shift-tab, or escape). done func(tcell.Key) + + // A callback function set by the Form class and called when the user leaves + // this form item. + finished func(tcell.Key) } // NewCheckbox returns a new input field. @@ -68,6 +76,13 @@ func (c *Checkbox) GetLabel() string { return c.label } +// SetLabelWidth sets the screen width of the label. A value of 0 will cause the +// primitive to use the width of the label string. +func (c *Checkbox) SetLabelWidth(width int) *Checkbox { + c.labelWidth = width + return c +} + // SetLabelColor sets the color of the label. func (c *Checkbox) SetLabelColor(color tcell.Color) *Checkbox { c.labelColor = color @@ -87,8 +102,8 @@ func (c *Checkbox) SetFieldTextColor(color tcell.Color) *Checkbox { } // SetFormAttributes sets attributes shared by all form items. -func (c *Checkbox) SetFormAttributes(label string, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem { - c.label = label +func (c *Checkbox) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem { + c.labelWidth = labelWidth c.labelColor = labelColor c.backgroundColor = bgColor c.fieldTextColor = fieldTextColor @@ -121,9 +136,10 @@ func (c *Checkbox) SetDoneFunc(handler func(key tcell.Key)) *Checkbox { return c } -// SetFinishedFunc calls SetDoneFunc(). +// SetFinishedFunc sets a callback invoked when the user leaves this form item. func (c *Checkbox) SetFinishedFunc(handler func(key tcell.Key)) FormItem { - return c.SetDoneFunc(handler) + c.finished = handler + return c } // Draw draws this primitive onto the screen. @@ -138,8 +154,17 @@ func (c *Checkbox) Draw(screen tcell.Screen) { } // Draw label. - _, drawnWidth := Print(screen, c.label, x, y, rightLimit-x, AlignLeft, c.labelColor) - x += drawnWidth + if c.labelWidth > 0 { + labelWidth := c.labelWidth + if labelWidth > rightLimit-x { + labelWidth = rightLimit - x + } + Print(screen, c.label, x, y, labelWidth, AlignLeft, c.labelColor) + x += labelWidth + } else { + _, drawnWidth := Print(screen, c.label, x, y, rightLimit-x, AlignLeft, c.labelColor) + x += drawnWidth + } // Draw checkbox. fieldStyle := tcell.StyleDefault.Background(c.fieldBackgroundColor).Foreground(c.fieldTextColor) @@ -170,6 +195,9 @@ func (c *Checkbox) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr if c.done != nil { c.done(key) } + if c.finished != nil { + c.finished(key) + } } }) } diff --git a/vendor/maunium.net/go/tview/doc.go b/vendor/maunium.net/go/tview/doc.go index 101dcd5..ccaaaf1 100644 --- a/vendor/maunium.net/go/tview/doc.go +++ b/vendor/maunium.net/go/tview/doc.go @@ -77,13 +77,53 @@ applies to almost everything from box titles, list text, form item labels, to table cells. In a TextView, this functionality has to be switched on explicitly. See the TextView documentation for more information. +Color tags may contain not just the foreground (text) color but also the +background color and additional flags. In fact, the full definition of a color +tag is as follows: + + [::] + +Each of the three fields can be left blank and trailing fields can be ommitted. +(Empty square brackets "[]", however, are not considered color tags.) Colors +that are not specified will be left unchanged. A field with just a dash ("-") +means "reset to default". + +You can specify the following flags (some flags may not be supported by your +terminal): + + l: blink + b: bold + d: dim + r: reverse (switch foreground and background color) + u: underline + +Examples: + + [yellow]Yellow text + [yellow:red]Yellow text on red background + [:red]Red background, text color unchanged + [yellow::u]Yellow text underlined + [::bl]Bold, blinking text + [::-]Colors unchanged, flags reset + [-]Reset foreground color + [-:-:-]Reset everything + [:]No effect + []Not a valid color tag, will print square brackets as they are + In the rare event that you want to display a string such as "[red]" or "[#00ff1a]" without applying its effect, you need to put an opening square -bracket before the closing square bracket. Examples: +bracket before the closing square bracket. Note that the text inside the +brackets will be matched less strictly than region or colors tags. I.e. any +character that may be used in color or region tags will be recognized. Examples: [red[] will be output as [red] ["123"[] will be output as ["123"] [#6aff00[[] will be output as [#6aff00[] + [a#"[[[] will be output as [a#"[[] + [] will be output as [] (see color tags above) + [[] will be output as [[] (not an escaped tag) + +You can use the Escape() function to insert brackets automatically where needed. Styles diff --git a/vendor/maunium.net/go/tview/dropdown.go b/vendor/maunium.net/go/tview/dropdown.go index 981d1bd..515f978 100644 --- a/vendor/maunium.net/go/tview/dropdown.go +++ b/vendor/maunium.net/go/tview/dropdown.go @@ -51,6 +51,10 @@ type DropDown struct { // The color for prefixes. prefixTextColor tcell.Color + // The screen width of the label area. A value of 0 means use the width of + // the label text. + labelWidth int + // The screen width of the input area. A value of 0 means extend as much as // possible. fieldWidth int @@ -59,6 +63,10 @@ type DropDown struct { // are done selecting options. The key which was pressed is provided (tab, // shift-tab, or escape). done func(tcell.Key) + + // A callback function set by the Form class and called when the user leaves + // this form item. + finished func(tcell.Key) } // NewDropDown returns a new drop-down. @@ -113,6 +121,13 @@ func (d *DropDown) GetLabel() string { return d.label } +// SetLabelWidth sets the screen width of the label. A value of 0 will cause the +// primitive to use the width of the label string. +func (d *DropDown) SetLabelWidth(width int) *DropDown { + d.labelWidth = width + return d +} + // SetLabelColor sets the color of the label. func (d *DropDown) SetLabelColor(color tcell.Color) *DropDown { d.labelColor = color @@ -140,8 +155,8 @@ func (d *DropDown) SetPrefixTextColor(color tcell.Color) *DropDown { } // SetFormAttributes sets attributes shared by all form items. -func (d *DropDown) SetFormAttributes(label string, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem { - d.label = label +func (d *DropDown) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem { + d.labelWidth = labelWidth d.labelColor = labelColor d.backgroundColor = bgColor d.fieldTextColor = fieldTextColor @@ -210,9 +225,10 @@ func (d *DropDown) SetDoneFunc(handler func(key tcell.Key)) *DropDown { return d } -// SetFinishedFunc calls SetDoneFunc(). +// SetFinishedFunc sets a callback invoked when the user leaves this form item. func (d *DropDown) SetFinishedFunc(handler func(key tcell.Key)) FormItem { - return d.SetDoneFunc(handler) + d.finished = handler + return d } // Draw draws this primitive onto the screen. @@ -227,8 +243,17 @@ func (d *DropDown) Draw(screen tcell.Screen) { } // Draw label. - _, drawnWidth := Print(screen, d.label, x, y, rightLimit-x, AlignLeft, d.labelColor) - x += drawnWidth + if d.labelWidth > 0 { + labelWidth := d.labelWidth + if labelWidth > rightLimit-x { + labelWidth = rightLimit - x + } + Print(screen, d.label, x, y, labelWidth, AlignLeft, d.labelColor) + x += labelWidth + } else { + _, drawnWidth := Print(screen, d.label, x, y, rightLimit-x, AlignLeft, d.labelColor) + x += drawnWidth + } // What's the longest option text? maxWidth := 0 @@ -359,6 +384,9 @@ func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr if d.done != nil { d.done(key) } + if d.finished != nil { + d.finished(key) + } } }) } diff --git a/vendor/maunium.net/go/tview/flex.go b/vendor/maunium.net/go/tview/flex.go index a698442..ad42d3a 100644 --- a/vendor/maunium.net/go/tview/flex.go +++ b/vendor/maunium.net/go/tview/flex.go @@ -69,8 +69,8 @@ func (f *Flex) SetFullScreen(fullScreen bool) *Flex { // that its size is flexible and may be changed. The "proportion" argument // defines the relative size of the item compared to other flexible-size items. // For example, items with a proportion of 2 will be twice as large as items -// with a proportion of 1. Must be at least 1 if fixedSize > 0 (ignored -// otherwise) +// with a proportion of 1. The proportion must be at least 1 if fixedSize == 0 +// (ignored otherwise). // // If "focus" is set to true, the item will receive focus when the Flex // primitive receives focus. If multiple items have the "focus" flag set to diff --git a/vendor/maunium.net/go/tview/form.go b/vendor/maunium.net/go/tview/form.go index b1e8f5e..fe0e980 100644 --- a/vendor/maunium.net/go/tview/form.go +++ b/vendor/maunium.net/go/tview/form.go @@ -1,8 +1,6 @@ package tview import ( - "strings" - "maunium.net/go/tcell" ) @@ -20,7 +18,7 @@ type FormItem interface { GetLabel() string // SetFormAttributes sets a number of item attributes at once. - SetFormAttributes(label string, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem + SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem // GetFieldWidth returns the width of the form item's field (the area which // is manipulated by the user) in number of screen cells. A value of 0 @@ -233,7 +231,14 @@ func (f *Form) Clear(includeButtons bool) *Form { // AddFormItem adds a new item to the form. This can be used to add your own // objects to the form. Note, however, that the Form class will override some -// of its attributes to make it work in the form context. +// of its attributes to make it work in the form context. Specifically, these +// are: +// +// - The label width +// - The label color +// - The background color +// - The field text color +// - The field background color func (f *Form) AddFormItem(item FormItem) *Form { f.items = append(f.items, item) return f @@ -246,6 +251,18 @@ func (f *Form) GetFormItem(index int) FormItem { return f.items[index] } +// GetFormItemByLabel returns the first form element with the given label. If +// no such element is found, nil is returned. Buttons are not searched and will +// therefore not be returned. +func (f *Form) GetFormItemByLabel(label string) FormItem { + for _, item := range f.items { + if item.GetLabel() == label { + return item + } + } + return nil +} + // SetCancelFunc sets a handler which is called when the user hits the Escape // key. func (f *Form) SetCancelFunc(callback func()) *Form { @@ -267,8 +284,7 @@ func (f *Form) Draw(screen tcell.Screen) { // Find the longest label. var maxLabelWidth int for _, item := range f.items { - label := strings.TrimSpace(item.GetLabel()) - labelWidth := StringWidth(label) + labelWidth := StringWidth(item.GetLabel()) if labelWidth > maxLabelWidth { maxLabelWidth = labelWidth } @@ -280,20 +296,18 @@ func (f *Form) Draw(screen tcell.Screen) { var focusedPosition struct{ x, y, width, height int } for index, item := range f.items { // Calculate the space needed. - label := strings.TrimSpace(item.GetLabel()) - labelWidth := StringWidth(label) + labelWidth := StringWidth(item.GetLabel()) var itemWidth int if f.horizontal { fieldWidth := item.GetFieldWidth() if fieldWidth == 0 { fieldWidth = DefaultFormFieldWidth } - label += " " labelWidth++ itemWidth = labelWidth + fieldWidth } else { // We want all fields to align vertically. - label += strings.Repeat(" ", maxLabelWidth-labelWidth) + labelWidth = maxLabelWidth itemWidth = width } @@ -308,7 +322,7 @@ func (f *Form) Draw(screen tcell.Screen) { itemWidth = rightLimit - x } item.SetFormAttributes( - label, + labelWidth, f.labelColor, f.backgroundColor, f.fieldTextColor, diff --git a/vendor/maunium.net/go/tview/grid.go b/vendor/maunium.net/go/tview/grid.go index 719ac7a..77797ba 100644 --- a/vendor/maunium.net/go/tview/grid.go +++ b/vendor/maunium.net/go/tview/grid.go @@ -258,7 +258,7 @@ func (g *Grid) HasFocus() bool { return true } } - return false + return g.hasFocus } // InputHandler returns the handler for this primitive. diff --git a/vendor/maunium.net/go/tview/inputfield.go b/vendor/maunium.net/go/tview/inputfield.go index c168106..2c92a93 100644 --- a/vendor/maunium.net/go/tview/inputfield.go +++ b/vendor/maunium.net/go/tview/inputfield.go @@ -41,6 +41,10 @@ type InputField struct { // The text color of the placeholder. placeholderTextColor tcell.Color + // The screen width of the label area. A value of 0 means use the width of + // the label text. + labelWidth int + // The screen width of the input area. A value of 0 means extend as much as // possible. fieldWidth int @@ -59,6 +63,10 @@ type InputField struct { // are done entering text. The key which was pressed is provided (tab, // shift-tab, enter, or escape). done func(tcell.Key) + + // A callback function set by the Form class and called when the user leaves + // this form item. + finished func(tcell.Key) } // NewInputField returns a new input field. @@ -97,6 +105,13 @@ func (i *InputField) GetLabel() string { return i.label } +// SetLabelWidth sets the screen width of the label. A value of 0 will cause the +// primitive to use the width of the label string. +func (i *InputField) SetLabelWidth(width int) *InputField { + i.labelWidth = width + return i +} + // SetPlaceholder sets the text to be displayed when the input text is empty. func (i *InputField) SetPlaceholder(text string) *InputField { i.placeholder = text @@ -121,15 +136,15 @@ func (i *InputField) SetFieldTextColor(color tcell.Color) *InputField { return i } -// SetPlaceholderExtColor sets the text color of placeholder text. -func (i *InputField) SetPlaceholderExtColor(color tcell.Color) *InputField { +// SetPlaceholderTextColor sets the text color of placeholder text. +func (i *InputField) SetPlaceholderTextColor(color tcell.Color) *InputField { i.placeholderTextColor = color return i } // SetFormAttributes sets attributes shared by all form items. -func (i *InputField) SetFormAttributes(label string, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem { - i.label = label +func (i *InputField) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem { + i.labelWidth = labelWidth i.labelColor = labelColor i.backgroundColor = bgColor i.fieldTextColor = fieldTextColor @@ -186,9 +201,10 @@ func (i *InputField) SetDoneFunc(handler func(key tcell.Key)) *InputField { return i } -// SetFinishedFunc calls SetDoneFunc(). +// SetFinishedFunc sets a callback invoked when the user leaves this form item. func (i *InputField) SetFinishedFunc(handler func(key tcell.Key)) FormItem { - return i.SetDoneFunc(handler) + i.finished = handler + return i } // Draw draws this primitive onto the screen. @@ -203,8 +219,17 @@ func (i *InputField) Draw(screen tcell.Screen) { } // Draw label. - _, drawnWidth := Print(screen, i.label, x, y, rightLimit-x, AlignLeft, i.labelColor) - x += drawnWidth + if i.labelWidth > 0 { + labelWidth := i.labelWidth + if labelWidth > rightLimit-x { + labelWidth = rightLimit - x + } + Print(screen, i.label, x, y, labelWidth, AlignLeft, i.labelColor) + x += labelWidth + } else { + _, drawnWidth := Print(screen, i.label, x, y, rightLimit-x, AlignLeft, i.labelColor) + x += drawnWidth + } // Draw input area. fieldWidth := i.fieldWidth @@ -280,7 +305,11 @@ func (i *InputField) setCursor(screen tcell.Screen) { if i.fieldWidth > 0 && fieldWidth > i.fieldWidth-1 { fieldWidth = i.fieldWidth - 1 } - x += StringWidth(i.label) + fieldWidth + if i.labelWidth > 0 { + x += i.labelWidth + fieldWidth + } else { + x += StringWidth(i.label) + fieldWidth + } if x >= rightLimit { x = rightLimit - 1 } @@ -323,6 +352,9 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p if i.done != nil { i.done(key) } + if i.finished != nil { + i.finished(key) + } } }) } diff --git a/vendor/maunium.net/go/tview/list.go b/vendor/maunium.net/go/tview/list.go index 7395985..cc25262 100644 --- a/vendor/maunium.net/go/tview/list.go +++ b/vendor/maunium.net/go/tview/list.go @@ -173,6 +173,26 @@ func (l *List) AddItem(mainText, secondaryText string, shortcut rune, selected f return l } +// GetItemCount returns the number of items in the list. +func (l *List) GetItemCount() int { + return len(l.items) +} + +// GetItemText returns an item's texts (main and secondary). Panics if the index +// is out of range. +func (l *List) GetItemText(index int) (main, secondary string) { + return l.items[index].MainText, l.items[index].SecondaryText +} + +// SetItemText sets an item's main and secondary text. Panics if the index is +// out of range. +func (l *List) SetItemText(index int, main, secondary string) *List { + item := l.items[index] + item.MainText = main + item.SecondaryText = secondary + return l +} + // Clear removes all items from the list. func (l *List) Clear() *List { l.items = nil diff --git a/vendor/maunium.net/go/tview/primitive.go b/vendor/maunium.net/go/tview/primitive.go index a59033f..f5034c4 100644 --- a/vendor/maunium.net/go/tview/primitive.go +++ b/vendor/maunium.net/go/tview/primitive.go @@ -36,6 +36,8 @@ type Primitive interface { MouseHandler() func(event *tcell.EventMouse, setFocus func(p Primitive)) + PasteHandler() func(event *tcell.EventPaste) + // Focus is called by the application when the primitive receives focus. // Implementers may call delegate() to pass the focus on to another primitive. Focus(delegate func(p Primitive)) diff --git a/vendor/maunium.net/go/tview/table.go b/vendor/maunium.net/go/tview/table.go index 4f2336f..0446c6a 100644 --- a/vendor/maunium.net/go/tview/table.go +++ b/vendor/maunium.net/go/tview/table.go @@ -590,7 +590,7 @@ ColumnLoop: expansion := 0 for _, row := range rows { if cell := getCell(row, column); cell != nil { - cellWidth := StringWidth(cell.Text) + _, _, _, _, cellWidth := decomposeString(cell.Text) if cell.MaxWidth > 0 && cell.MaxWidth < cellWidth { cellWidth = cell.MaxWidth } diff --git a/vendor/maunium.net/go/tview/textview.go b/vendor/maunium.net/go/tview/textview.go index 8de0121..16b9dfb 100644 --- a/vendor/maunium.net/go/tview/textview.go +++ b/vendor/maunium.net/go/tview/textview.go @@ -8,6 +8,7 @@ import ( "unicode/utf8" "maunium.net/go/tcell" + "github.com/lucasb-eyer/go-colorful" runewidth "github.com/mattn/go-runewidth" ) @@ -17,12 +18,14 @@ var TabSize = 4 // textViewIndex contains information about each line displayed in the text // view. type textViewIndex struct { - Line int // The index into the "buffer" variable. - Pos int // The index into the "buffer" string (byte position). - NextPos int // The (byte) index of the next character in this buffer line. - Width int // The screen width of this line. - Color tcell.Color // The starting color. - Region string // The starting region ID. + Line int // The index into the "buffer" variable. + Pos int // The index into the "buffer" string (byte position). + NextPos int // The (byte) index of the next character in this buffer line. + Width int // The screen width of this line. + ForegroundColor string // The starting foreground color ("" = don't change, "-" = reset). + BackgroundColor string // The starting background color ("" = don't change, "-" = reset). + Attributes string // The starting attributes ("" = don't change, "-" = reset). + Region string // The starting region ID. } // TextView is a box which displays text. It implements the io.Writer interface @@ -499,7 +502,6 @@ func (t *TextView) reindexBuffer(width int) { // Initial states. regionID := "" var highlighted bool - color := t.textColor // Go through each line in the buffer. for bufferIndex, str := range t.buffer { @@ -507,11 +509,10 @@ func (t *TextView) reindexBuffer(width int) { var ( colorTagIndices [][]int colorTags [][]string + escapeIndices [][]int ) if t.dynamicColors { - colorTagIndices = colorPattern.FindAllStringIndex(str, -1) - colorTags = colorPattern.FindAllStringSubmatch(str, -1) - str = colorPattern.ReplaceAllString(str, "") + colorTagIndices, colorTags, escapeIndices, str, _ = decomposeString(str) } // Find all regions in this line. Then remove them. @@ -523,13 +524,11 @@ func (t *TextView) reindexBuffer(width int) { regionIndices = regionPattern.FindAllStringIndex(str, -1) regions = regionPattern.FindAllStringSubmatch(str, -1) str = regionPattern.ReplaceAllString(str, "") - } - - // Find all replace tags in this line. Then replace them. - var escapeIndices [][]int - if t.dynamicColors || t.regions { - escapeIndices = escapePattern.FindAllStringIndex(str, -1) - str = escapePattern.ReplaceAllString(str, "[$1$2]") + if !t.dynamicColors { + // We haven't detected escape tags yet. Do it now. + escapeIndices = escapePattern.FindAllStringIndex(str, -1) + str = escapePattern.ReplaceAllString(str, "[$1$2]") + } } // Split the line if required. @@ -559,13 +558,18 @@ func (t *TextView) reindexBuffer(width int) { } // Create index from split lines. - var originalPos, colorPos, regionPos, escapePos int + var ( + originalPos, colorPos, regionPos, escapePos int + foregroundColor, backgroundColor, attributes string + ) for _, splitLine := range splitLines { line := &textViewIndex{ - Line: bufferIndex, - Pos: originalPos, - Color: color, - Region: regionID, + Line: bufferIndex, + Pos: originalPos, + ForegroundColor: foregroundColor, + BackgroundColor: backgroundColor, + Attributes: attributes, + Region: regionID, } // Shift original position with tags. @@ -574,7 +578,7 @@ func (t *TextView) reindexBuffer(width int) { if colorPos < len(colorTagIndices) && colorTagIndices[colorPos][0] <= originalPos+lineLength { // Process color tags. originalPos += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0] - color = tcell.GetColor(colorTags[colorPos][1]) + foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[colorPos]) colorPos++ } else if regionPos < len(regionIndices) && regionIndices[regionPos][0] <= originalPos+lineLength { // Process region tags. @@ -712,6 +716,7 @@ func (t *TextView) Draw(screen tcell.Screen) { } // Draw the buffer. + defaultStyle := tcell.StyleDefault.Foreground(t.textColor) for line := t.lineOffset; line < len(t.index); line++ { // Are we done? if line-t.lineOffset >= height { @@ -721,17 +726,19 @@ func (t *TextView) Draw(screen tcell.Screen) { // Get the text for this line. index := t.index[line] text := t.buffer[index.Line][index.Pos:index.NextPos] - color := index.Color + foregroundColor := index.ForegroundColor + backgroundColor := index.BackgroundColor + attributes := index.Attributes regionID := index.Region // Get color tags. var ( colorTagIndices [][]int colorTags [][]string + escapeIndices [][]int ) if t.dynamicColors { - colorTagIndices = colorPattern.FindAllStringIndex(text, -1) - colorTags = colorPattern.FindAllStringSubmatch(text, -1) + colorTagIndices, colorTags, escapeIndices, _, _ = decomposeString(text) } // Get regions. @@ -742,12 +749,9 @@ func (t *TextView) Draw(screen tcell.Screen) { if t.regions { regionIndices = regionPattern.FindAllStringIndex(text, -1) regions = regionPattern.FindAllStringSubmatch(text, -1) - } - - // Get escape tags. - var escapeIndices [][]int - if t.dynamicColors || t.regions { - escapeIndices = escapePattern.FindAllStringIndex(text, -1) + if !t.dynamicColors { + escapeIndices = escapePattern.FindAllStringIndex(text, -1) + } } // Calculate the position of the line. @@ -770,7 +774,7 @@ func (t *TextView) Draw(screen tcell.Screen) { // Get the color. if currentTag < len(colorTags) && pos >= colorTagIndices[currentTag][0] && pos < colorTagIndices[currentTag][1] { if pos == colorTagIndices[currentTag][1]-1 { - color = tcell.GetColor(colorTags[currentTag][1]) + foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[currentTag]) currentTag++ } continue @@ -811,13 +815,32 @@ func (t *TextView) Draw(screen tcell.Screen) { break } + // Mix the existing style with the new style. + _, _, existingStyle, _ := screen.GetContent(x+posX, y+line-t.lineOffset) + _, background, _ := existingStyle.Decompose() + style := overlayStyle(background, defaultStyle, foregroundColor, backgroundColor, attributes) + // Do we highlight this character? - style := tcell.StyleDefault.Background(t.backgroundColor).Foreground(color) + var highlighted bool if len(regionID) > 0 { if _, ok := t.highlights[regionID]; ok { - style = tcell.StyleDefault.Background(color).Foreground(t.backgroundColor) + highlighted = true } } + if highlighted { + fg, bg, _ := style.Decompose() + if bg == tcell.ColorDefault { + r, g, b := fg.RGB() + c := colorful.Color{R: float64(r) / 255, G: float64(g) / 255, B: float64(b) / 255} + _, _, li := c.Hcl() + if li < .5 { + bg = tcell.ColorWhite + } else { + bg = tcell.ColorBlack + } + } + style = style.Background(fg).Foreground(bg) + } // Draw the character. for offset := 0; offset < chWidth; offset++ { diff --git a/vendor/maunium.net/go/tview/util.go b/vendor/maunium.net/go/tview/util.go index f49c35f..2b3a544 100644 --- a/vendor/maunium.net/go/tview/util.go +++ b/vendor/maunium.net/go/tview/util.go @@ -1,6 +1,7 @@ package tview import ( + "fmt" "math" "regexp" "strconv" @@ -104,11 +105,19 @@ var joints = map[string]rune{ // Common regular expressions. var ( - colorPattern = regexp.MustCompile(`\[([a-zA-Z]+|#[0-9a-zA-Z]{6})\]`) - regionPattern = regexp.MustCompile(`\["([a-zA-Z0-9_,;: \-\.]*)"\]`) - escapePattern = regexp.MustCompile(`\[("[a-zA-Z0-9_,;: \-\.]*"|[a-zA-Z]+|#[0-9a-zA-Z]{6})\[(\[*)\]`) - boundaryPattern = regexp.MustCompile("([[:punct:]]\\s*|\\s+)") - spacePattern = regexp.MustCompile(`\s+`) + colorPattern = regexp.MustCompile(`\[([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([lbdru]+|\-)?)?)?\]`) + 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+)") + spacePattern = regexp.MustCompile(`\s+`) +) + +// Positions of substrings in regular expressions. +const ( + colorForegroundPos = 1 + colorBackgroundPos = 3 + colorFlagPos = 5 ) // Predefined InputField acceptance functions. @@ -150,40 +159,168 @@ 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 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(background tcell.Color, defaultStyle tcell.Style, fgColor, bgColor, attributes string) tcell.Style { + defFg, defBg, defAttr := defaultStyle.Decompose() + style := defaultStyle.Background(background) + + if fgColor == "-" { + style = style.Foreground(defFg) + } else if fgColor != "" { + style = style.Foreground(tcell.GetColor(fgColor)) + } + + if bgColor == "-" { + style = style.Background(defBg) + } else if bgColor != "" { + style = style.Background(tcell.GetColor(bgColor)) + } + + if attributes == "-" { + style = style.Bold(defAttr&tcell.AttrBold > 0) + style = style.Blink(defAttr&tcell.AttrBlink > 0) + style = style.Reverse(defAttr&tcell.AttrReverse > 0) + style = style.Underline(defAttr&tcell.AttrUnderline > 0) + style = style.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 'd': + style = style.Dim(true) + case 'r': + style = style.Reverse(true) + case 'u': + style = style.Underline(true) + } + } + } + + return style +} + +// decomposeString returns information about a string which may contain color +// tags. It returns the indices of the color tags (as returned by +// re.FindAllStringIndex()), the color tags themselves (as returned by +// re.FindAllStringSubmatch()), the indices of an escaped tags, the string +// stripped by any color tags and escaped, and the screen width of the stripped +// string. +func decomposeString(text string) (colorIndices [][]int, colors [][]string, escapeIndices [][]int, stripped string, width int) { + // Get positions of color and escape tags. + colorIndices = colorPattern.FindAllStringIndex(text, -1) + colors = colorPattern.FindAllStringSubmatch(text, -1) + 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:]...) + } + } + + // Remove the color tags from the original string. + var from int + buf := make([]byte, 0, len(text)) + for _, indices := range colorIndices { + buf = append(buf, []byte(text[from:indices[0]])...) + from = indices[1] + } + buf = append(buf, text[from:]...) + + // Escape string. + stripped = string(escapePattern.ReplaceAll(buf, []byte("[$1$2]"))) + + // Get the width of the stripped string. + width = runewidth.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. // -// You can change the text color mid-text by inserting a color tag. See the -// package description for details. +// 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. 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)) +} + +// printWithStyle works like Print() but it takes a style instead of just a +// foreground color. +func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, style tcell.Style) (int, int) { if maxWidth < 0 { return 0, 0 } - // Get positions of color and escape tags. Remove them from original string. - colorIndices := colorPattern.FindAllStringIndex(text, -1) - colors := colorPattern.FindAllStringSubmatch(text, -1) - escapeIndices := escapePattern.FindAllStringIndex(text, -1) - strippedText := escapePattern.ReplaceAllString(colorPattern.ReplaceAllString(text, ""), "[$1$2]") + // Decompose the text. + colorIndices, colors, escapeIndices, strippedText, _ := decomposeString(text) // We deal with runes, not with bytes. runes := []rune(strippedText) - // This helper function takes positions for a substring of "runes" and a start - // color and returns the substring with the original tags and the new start - // color. - substring := func(from, to int, color tcell.Color) (string, tcell.Color) { - var colorPos, escapePos, runePos, startPos int + // 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 { + var ( + colorPos, escapePos, runePos, startPos int + foregroundColor, backgroundColor, attributes string + ) 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 { - color = tcell.GetColor(colors[colorPos][1]) + foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos]) } colorPos++ } @@ -203,13 +340,13 @@ func Print(screen tcell.Screen, text string, x, y, maxWidth, align int, color tc if runePos == from { startPos = pos } else if runePos >= to { - return text[startPos:pos], color + return fmt.Sprintf(`[%s:%s:%s]%s`, foregroundColor, backgroundColor, attributes, text[startPos:pos]) } runePos++ } - return text[startPos:], color + return fmt.Sprintf(`[%s:%s:%s]%s`, foregroundColor, backgroundColor, attributes, text[startPos:]) } // We want to reduce everything to AlignLeft. @@ -224,17 +361,16 @@ func Print(screen tcell.Screen, text string, x, y, maxWidth, align int, color tc width += w start = index } - text, color = substring(start, len(runes), color) - return Print(screen, text, x+maxWidth-width, y, width, AlignLeft, color) + return printWithStyle(screen, substring(start, len(runes)), x+maxWidth-width, y, width, AlignLeft, style) } else if align == AlignCenter { width := runewidth.StringWidth(strippedText) if width == maxWidth { // Use the exact space. - return Print(screen, text, x, y, maxWidth, AlignLeft, color) + return printWithStyle(screen, text, x, y, maxWidth, AlignLeft, style) } else if width < maxWidth { // We have more space than we need. half := (maxWidth - width) / 2 - return Print(screen, text, x+half, y, maxWidth-half, AlignLeft, color) + 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 @@ -250,20 +386,22 @@ func Print(screen tcell.Screen, text string, x, y, maxWidth, align int, color tc rightIndex-- } } - text, color = substring(leftIndex, rightIndex, color) - return Print(screen, text, x, y, maxWidth, AlignLeft, color) + return printWithStyle(screen, substring(leftIndex, rightIndex), x, y, maxWidth, AlignLeft, style) } } // Draw text. drawn := 0 drawnWidth := 0 - var colorPos, escapePos int + var ( + colorPos, escapePos int + foregroundColor, backgroundColor, attributes string + ) for pos, ch := range text { // Handle color tags. if colorPos < len(colorIndices) && pos >= colorIndices[colorPos][0] && pos < colorIndices[colorPos][1] { if pos == colorIndices[colorPos][1]-1 { - color = tcell.GetColor(colors[colorPos][1]) + foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos]) colorPos++ } continue @@ -286,11 +424,12 @@ func Print(screen tcell.Screen, text string, x, y, maxWidth, align int, color tc finalX := x + drawnWidth // Print the rune. - _, _, style, _ := screen.GetContent(finalX, y) - style = style.Foreground(color) + _, _, finalStyle, _ := screen.GetContent(finalX, y) + _, background, _ := finalStyle.Decompose() + finalStyle = overlayStyle(background, style, foregroundColor, backgroundColor, attributes) for offset := 0; offset < chWidth; offset++ { // To avoid undesired effects, we place the same character in all cells. - screen.SetContent(finalX+offset, y, ch, nil, style) + screen.SetContent(finalX+offset, y, ch, nil, finalStyle) } drawn++ @@ -308,7 +447,8 @@ func PrintSimple(screen tcell.Screen, text string, x, y int) { // StringWidth returns the width of the given string needed to print it on // screen. The text may contain color tags which are not counted. func StringWidth(text string) int { - return runewidth.StringWidth(escapePattern.ReplaceAllString(colorPattern.ReplaceAllString(text, ""), "[$1$2]")) + _, _, _, _, width := decomposeString(text) + return width } // WordWrap splits a text such that each resulting line does not exceed the @@ -319,13 +459,7 @@ func StringWidth(text string) int { // // Text is always split at newline characters ('\n'). func WordWrap(text string, width int) (lines []string) { - // Strip color tags. - strippedText := escapePattern.ReplaceAllString(colorPattern.ReplaceAllString(text, ""), "[$1$2]") - - // Keep track of color tags and escape patterns so we can restore the original - // indices. - colorTagIndices := colorPattern.FindAllStringIndex(text, -1) - escapeIndices := escapePattern.FindAllStringIndex(text, -1) + colorTagIndices, _, escapeIndices, strippedText, _ := decomposeString(text) // Find candidate breakpoints. breakPoints := boundaryPattern.FindAllStringIndex(strippedText, -1) @@ -454,3 +588,13 @@ func PrintJoinedBorder(screen tcell.Screen, x, y int, ch rune, color tcell.Color // We only print something if we have something. screen.SetContent(x, y, result, nil, style) } + +// 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[]") +}