aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/github.com/MichaelMure/go-term-text
diff options
context:
space:
mode:
authorMichael Muré <batolettre@gmail.com>2019-11-03 13:09:55 +0000
committerGitHub <noreply@github.com>2019-11-03 13:09:55 +0000
commit8c7c9880b1adcf876ad63ea39e46e62bd7ebde5d (patch)
treef9c6369a6593e3c00cfb3c0b45009dfb6d055d7b /vendor/github.com/MichaelMure/go-term-text
parentf5193cc76dd1a1c1bb55194a64ef90bae2278115 (diff)
parent912b5ca320891f2a4f1a88f1a137ce8ee46a1a03 (diff)
downloadgit-bug-8c7c9880b1adcf876ad63ea39e46e62bd7ebde5d.tar.gz
Merge pull request #228 from ludovicm67/patch-cli-label-colors
Display label colors in termui
Diffstat (limited to 'vendor/github.com/MichaelMure/go-term-text')
-rw-r--r--vendor/github.com/MichaelMure/go-term-text/.gitignore1
-rw-r--r--vendor/github.com/MichaelMure/go-term-text/.travis.yml16
-rw-r--r--vendor/github.com/MichaelMure/go-term-text/LICENSE21
-rw-r--r--vendor/github.com/MichaelMure/go-term-text/Readme.md71
-rw-r--r--vendor/github.com/MichaelMure/go-term-text/align.go67
-rw-r--r--vendor/github.com/MichaelMure/go-term-text/escapes.go95
-rw-r--r--vendor/github.com/MichaelMure/go-term-text/go.mod8
-rw-r--r--vendor/github.com/MichaelMure/go-term-text/go.sum9
-rw-r--r--vendor/github.com/MichaelMure/go-term-text/left_pad.go50
-rw-r--r--vendor/github.com/MichaelMure/go-term-text/len.go45
-rw-r--r--vendor/github.com/MichaelMure/go-term-text/trim.go28
-rw-r--r--vendor/github.com/MichaelMure/go-term-text/truncate.go24
-rw-r--r--vendor/github.com/MichaelMure/go-term-text/wrap.go334
13 files changed, 769 insertions, 0 deletions
diff --git a/vendor/github.com/MichaelMure/go-term-text/.gitignore b/vendor/github.com/MichaelMure/go-term-text/.gitignore
new file mode 100644
index 00000000..9f11b755
--- /dev/null
+++ b/vendor/github.com/MichaelMure/go-term-text/.gitignore
@@ -0,0 +1 @@
+.idea/
diff --git a/vendor/github.com/MichaelMure/go-term-text/.travis.yml b/vendor/github.com/MichaelMure/go-term-text/.travis.yml
new file mode 100644
index 00000000..496ca056
--- /dev/null
+++ b/vendor/github.com/MichaelMure/go-term-text/.travis.yml
@@ -0,0 +1,16 @@
+language: go
+
+go:
+ - 1.10.x
+ - 1.11.x
+ - 1.12.x
+
+env:
+ - GO111MODULE=on
+
+script:
+ - go build
+ - go test -v -bench=. -race -coverprofile=coverage.txt -covermode=atomic ./...
+
+after_success:
+ - bash <(curl -s https://codecov.io/bash)
diff --git a/vendor/github.com/MichaelMure/go-term-text/LICENSE b/vendor/github.com/MichaelMure/go-term-text/LICENSE
new file mode 100644
index 00000000..5ba12bf4
--- /dev/null
+++ b/vendor/github.com/MichaelMure/go-term-text/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 Michael Muré
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vendor/github.com/MichaelMure/go-term-text/Readme.md b/vendor/github.com/MichaelMure/go-term-text/Readme.md
new file mode 100644
index 00000000..457b4472
--- /dev/null
+++ b/vendor/github.com/MichaelMure/go-term-text/Readme.md
@@ -0,0 +1,71 @@
+# go-term-text
+
+[![Build Status](https://travis-ci.org/MichaelMure/go-term-text.svg?branch=master)](https://travis-ci.org/MichaelMure/go-term-text)
+[![GoDoc](https://godoc.org/github.com/MichaelMure/go-term-text?status.svg)](https://godoc.org/github.com/MichaelMure/go-term-text)
+[![Go Report Card](https://goreportcard.com/badge/github.com/MichaelMure/go-term-text)](https://goreportcard.com/report/github.com/MichaelMure/go-term-text)
+[![codecov](https://codecov.io/gh/MichaelMure/go-term-text/branch/master/graph/badge.svg)](https://codecov.io/gh/MichaelMure/go-term-text)
+[![GitHub license](https://img.shields.io/github/license/MichaelMure/go-term-text.svg)](https://github.com/MichaelMure/go-term-text/blob/master/LICENSE)
+[![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/the-git-bug/Lobby)
+
+`go-term-text` is a go package implementing a collection of algorithms to help format and manipulate text for the terminal.
+
+In particular, `go-term-text`:
+- support wide characters (chinese, japanese ...) and emoji
+- handle properly ANSI escape sequences
+
+Included algorithms cover:
+- wrapping with padding and indentation
+- padding
+- text length
+- trimming
+- alignment
+- escape sequence extraction and reapplication
+- truncation
+
+## Example
+
+```go
+package main
+
+import (
+ "fmt"
+ "strings"
+
+ text "github.com/MichaelMure/go-term-text"
+)
+
+func main() {
+ input := "The \x1b[1mLorem ipsum\x1b[0m text is typically composed of " +
+ "pseudo-Latin words. It is commonly used as \x1b[3mplaceholder\x1b[0m" +
+ " text to examine or demonstrate the \x1b[9mvisual effects\x1b[0m of " +
+ "various graphic design. 一只 A Quick \x1b[31m敏捷的狐 Fox " +
+ "狸跳过了\x1b[0mDog一只懒狗。"
+
+ output, n := text.WrapWithPadIndent(input, 60,
+ "\x1b[34m<-indent-> \x1b[0m", "\x1b[33m<-pad-> \x1b[0m")
+
+ fmt.Printf("output has %d lines\n\n", n)
+
+ fmt.Println("|" + strings.Repeat("-", 58) + "|")
+ fmt.Println(output)
+ fmt.Println("|" + strings.Repeat("-", 58) + "|")
+}
+```
+
+This will print:
+
+![example output](/img/example.png)
+
+For more details, have a look at the [GoDoc](https://godoc.org/github.com/MichaelMure/go-term-text).
+
+## Origin
+
+This package has been extracted from the [git-bug](https://github.com/MichaelMure/git-bug) project. As such, its aim is to support this project and not to provide an all-in-one solution. Contributions as welcome though.
+
+## Contribute
+
+PRs accepted.
+
+## License
+
+MIT
diff --git a/vendor/github.com/MichaelMure/go-term-text/align.go b/vendor/github.com/MichaelMure/go-term-text/align.go
new file mode 100644
index 00000000..8262a4de
--- /dev/null
+++ b/vendor/github.com/MichaelMure/go-term-text/align.go
@@ -0,0 +1,67 @@
+package text
+
+import (
+ "strings"
+)
+
+type Alignment int
+
+const (
+ NoAlign Alignment = iota
+ AlignLeft
+ AlignCenter
+ AlignRight
+)
+
+// LineAlign align the given line as asked and apply the needed padding to match the given
+// lineWidth, while ignoring the terminal escape sequences.
+// If the given lineWidth is too small to fit the given line, it's returned without
+// padding, overflowing lineWidth.
+func LineAlign(line string, lineWidth int, align Alignment) string {
+ switch align {
+ case NoAlign:
+ return line
+ case AlignLeft:
+ return LineAlignLeft(line, lineWidth)
+ case AlignCenter:
+ return LineAlignCenter(line, lineWidth)
+ case AlignRight:
+ return LineAlignRight(line, lineWidth)
+ }
+ panic("unknown alignment")
+}
+
+// LineAlignLeft align the given line on the left while ignoring the terminal escape sequences.
+// If the given lineWidth is too small to fit the given line, it's returned without
+// padding, overflowing lineWidth.
+func LineAlignLeft(line string, lineWidth int) string {
+ return TrimSpace(line)
+}
+
+// LineAlignCenter align the given line on the center and apply the needed left
+// padding, while ignoring the terminal escape sequences.
+// If the given lineWidth is too small to fit the given line, it's returned without
+// padding, overflowing lineWidth.
+func LineAlignCenter(line string, lineWidth int) string {
+ trimmed := TrimSpace(line)
+ totalPadLen := lineWidth - Len(trimmed)
+ if totalPadLen < 0 {
+ totalPadLen = 0
+ }
+ pad := strings.Repeat(" ", totalPadLen/2)
+ return pad + trimmed
+}
+
+// LineAlignRight align the given line on the right and apply the needed left
+// padding to match the given lineWidth, while ignoring the terminal escape sequences.
+// If the given lineWidth is too small to fit the given line, it's returned without
+// padding, overflowing lineWidth.
+func LineAlignRight(line string, lineWidth int) string {
+ trimmed := TrimSpace(line)
+ padLen := lineWidth - Len(trimmed)
+ if padLen < 0 {
+ padLen = 0
+ }
+ pad := strings.Repeat(" ", padLen)
+ return pad + trimmed
+}
diff --git a/vendor/github.com/MichaelMure/go-term-text/escapes.go b/vendor/github.com/MichaelMure/go-term-text/escapes.go
new file mode 100644
index 00000000..19f78c92
--- /dev/null
+++ b/vendor/github.com/MichaelMure/go-term-text/escapes.go
@@ -0,0 +1,95 @@
+package text
+
+import (
+ "strings"
+ "unicode/utf8"
+)
+
+// EscapeItem hold the description of terminal escapes in a line.
+// 'item' is the actual escape command
+// 'pos' is the index in the rune array where the 'item' shall be inserted back.
+// For example, the escape item in "F\x1b33mox" is {"\x1b33m", 1}.
+type EscapeItem struct {
+ Item string
+ Pos int
+}
+
+// ExtractTermEscapes extract terminal escapes out of a line and returns a new
+// line without terminal escapes and a slice of escape items. The terminal escapes
+// can be inserted back into the new line at rune index 'item.pos' to recover the
+// original line.
+//
+// Required: The line shall not contain "\n"
+func ExtractTermEscapes(line string) (string, []EscapeItem) {
+ var termEscapes []EscapeItem
+ var line1 strings.Builder
+
+ pos := 0
+ item := ""
+ occupiedRuneCount := 0
+ inEscape := false
+ for i, r := range []rune(line) {
+ if r == '\x1b' {
+ pos = i
+ item = string(r)
+ inEscape = true
+ continue
+ }
+ if inEscape {
+ item += string(r)
+ if r == 'm' {
+ termEscapes = append(termEscapes, EscapeItem{item, pos - occupiedRuneCount})
+ occupiedRuneCount += utf8.RuneCountInString(item)
+ inEscape = false
+ }
+ continue
+ }
+ line1.WriteRune(r)
+ }
+
+ return line1.String(), termEscapes
+}
+
+// ApplyTermEscapes apply the extracted terminal escapes to the edited line.
+// Escape sequences need to be ordered by their position.
+// If the position is < 0, the escape is applied at the beginning of the line.
+// If the position is > len(line), the escape is applied at the end of the line.
+func ApplyTermEscapes(line string, escapes []EscapeItem) string {
+ if len(escapes) == 0 {
+ return line
+ }
+
+ var out strings.Builder
+
+ currPos := 0
+ currItem := 0
+ for _, r := range line {
+ for currItem < len(escapes) && currPos >= escapes[currItem].Pos {
+ out.WriteString(escapes[currItem].Item)
+ currItem++
+ }
+ out.WriteRune(r)
+ currPos++
+ }
+
+ // Don't forget the trailing escapes, if any.
+ for currItem < len(escapes) {
+ out.WriteString(escapes[currItem].Item)
+ currItem++
+ }
+
+ return out.String()
+}
+
+// OffsetEscapes is a utility function to offset the position of a
+// collection of EscapeItem.
+func OffsetEscapes(escapes []EscapeItem, offset int) []EscapeItem {
+ result := make([]EscapeItem, len(escapes))
+ for i, e := range escapes {
+ result[i] = EscapeItem{
+ Item: e.Item,
+ Pos: e.Pos + offset,
+ }
+ }
+ return result
+}
diff --git a/vendor/github.com/MichaelMure/go-term-text/go.mod b/vendor/github.com/MichaelMure/go-term-text/go.mod
new file mode 100644
index 00000000..162c5dac
--- /dev/null
+++ b/vendor/github.com/MichaelMure/go-term-text/go.mod
@@ -0,0 +1,8 @@
+module github.com/MichaelMure/go-term-text
+
+go 1.10
+
+require (
+ github.com/mattn/go-runewidth v0.0.4
+ github.com/stretchr/testify v1.3.0
+)
diff --git a/vendor/github.com/MichaelMure/go-term-text/go.sum b/vendor/github.com/MichaelMure/go-term-text/go.sum
new file mode 100644
index 00000000..0aaedf16
--- /dev/null
+++ b/vendor/github.com/MichaelMure/go-term-text/go.sum
@@ -0,0 +1,9 @@
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
+github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
diff --git a/vendor/github.com/MichaelMure/go-term-text/left_pad.go b/vendor/github.com/MichaelMure/go-term-text/left_pad.go
new file mode 100644
index 00000000..a63fedb9
--- /dev/null
+++ b/vendor/github.com/MichaelMure/go-term-text/left_pad.go
@@ -0,0 +1,50 @@
+package text
+
+import (
+ "bytes"
+ "strings"
+
+ "github.com/mattn/go-runewidth"
+)
+
+// LeftPadMaxLine pads a line on the left by a specified amount and pads the
+// string on the right to fill the maxLength.
+// If the given string is too long, it is truncated with an ellipsis.
+// Handle properly terminal color escape code
+func LeftPadMaxLine(line string, length, leftPad int) string {
+ cleaned, escapes := ExtractTermEscapes(line)
+
+ scrWidth := runewidth.StringWidth(cleaned)
+ // truncate and ellipse if needed
+ if scrWidth+leftPad > length {
+ cleaned = runewidth.Truncate(cleaned, length-leftPad, "…")
+ } else if scrWidth+leftPad < length {
+ cleaned = runewidth.FillRight(cleaned, length-leftPad)
+ }
+
+ rightPart := ApplyTermEscapes(cleaned, escapes)
+ pad := strings.Repeat(" ", leftPad)
+
+ return pad + rightPart
+}
+
+// LeftPad left pad each line of the given text
+func LeftPadLines(text string, leftPad int) string {
+ var result bytes.Buffer
+
+ pad := strings.Repeat(" ", leftPad)
+
+ lines := strings.Split(text, "\n")
+
+ for i, line := range lines {
+ result.WriteString(pad)
+ result.WriteString(line)
+
+ // no additional line break at the end
+ if i < len(lines)-1 {
+ result.WriteString("\n")
+ }
+ }
+
+ return result.String()
+}
diff --git a/vendor/github.com/MichaelMure/go-term-text/len.go b/vendor/github.com/MichaelMure/go-term-text/len.go
new file mode 100644
index 00000000..c6bcaeac
--- /dev/null
+++ b/vendor/github.com/MichaelMure/go-term-text/len.go
@@ -0,0 +1,45 @@
+package text
+
+import (
+ "strings"
+
+ "github.com/mattn/go-runewidth"
+)
+
+// Len return the length of a string in a terminal, while ignoring the terminal
+// escape sequences.
+func Len(text string) int {
+ length := 0
+ escape := false
+
+ for _, char := range text {
+ if char == '\x1b' {
+ escape = true
+ }
+ if !escape {
+ length += runewidth.RuneWidth(char)
+ }
+ if char == 'm' {
+ escape = false
+ }
+ }
+
+ return length
+}
+
+// MaxLineLen return the length in a terminal of the longest line, while
+// ignoring the terminal escape sequences.
+func MaxLineLen(text string) int {
+ lines := strings.Split(text, "\n")
+
+ max := 0
+
+ for _, line := range lines {
+ length := Len(line)
+ if length > max {
+ max = length
+ }
+ }
+
+ return max
+}
diff --git a/vendor/github.com/MichaelMure/go-term-text/trim.go b/vendor/github.com/MichaelMure/go-term-text/trim.go
new file mode 100644
index 00000000..eaf2ca0c
--- /dev/null
+++ b/vendor/github.com/MichaelMure/go-term-text/trim.go
@@ -0,0 +1,28 @@
+package text
+
+import (
+ "strings"
+ "unicode"
+)
+
+// TrimSpace remove the leading and trailing whitespace while ignoring the
+// terminal escape sequences.
+// Returns the number of trimmed space on both side.
+func TrimSpace(line string) string {
+ cleaned, escapes := ExtractTermEscapes(line)
+
+ // trim left while counting
+ left := 0
+ trimmed := strings.TrimLeftFunc(cleaned, func(r rune) bool {
+ if unicode.IsSpace(r) {
+ left++
+ return true
+ }
+ return false
+ })
+
+ trimmed = strings.TrimRightFunc(trimmed, unicode.IsSpace)
+
+ escapes = OffsetEscapes(escapes, -left)
+ return ApplyTermEscapes(trimmed, escapes)
+}
diff --git a/vendor/github.com/MichaelMure/go-term-text/truncate.go b/vendor/github.com/MichaelMure/go-term-text/truncate.go
new file mode 100644
index 00000000..b51bb39e
--- /dev/null
+++ b/vendor/github.com/MichaelMure/go-term-text/truncate.go
@@ -0,0 +1,24 @@
+package text
+
+import "github.com/mattn/go-runewidth"
+
+// TruncateMax truncate a line if its length is greater
+// than the given length. Otherwise, the line is returned
+// as is. If truncating occur, an ellipsis is inserted at
+// the end.
+// Handle properly terminal color escape code
+func TruncateMax(line string, length int) string {
+ if length <= 0 {
+ return "…"
+ }
+
+ l := Len(line)
+ if l <= length || l == 0 {
+ return line
+ }
+
+ cleaned, escapes := ExtractTermEscapes(line)
+ truncated := runewidth.Truncate(cleaned, length-1, "")
+
+ return ApplyTermEscapes(truncated, escapes) + "…"
+}
diff --git a/vendor/github.com/MichaelMure/go-term-text/wrap.go b/vendor/github.com/MichaelMure/go-term-text/wrap.go
new file mode 100644
index 00000000..2fd6ed5f
--- /dev/null
+++ b/vendor/github.com/MichaelMure/go-term-text/wrap.go
@@ -0,0 +1,334 @@
+package text
+
+import (
+ "strings"
+
+ "github.com/mattn/go-runewidth"
+)
+
+// Force runewidth not to treat ambiguous runes as wide chars, so that things
+// like unicode ellipsis/up/down/left/right glyphs can have correct runewidth
+// and can be displayed correctly in terminals.
+func init() {
+ runewidth.DefaultCondition.EastAsianWidth = false
+}
+
+// Wrap a text for a given line size.
+// Handle properly terminal color escape code
+func Wrap(text string, lineWidth int) (string, int) {
+ return WrapLeftPadded(text, lineWidth, 0)
+}
+
+// WrapLeftPadded wrap a text for a given line size with a left padding.
+// Handle properly terminal color escape code
+func WrapLeftPadded(text string, lineWidth int, leftPad int) (string, int) {
+ pad := strings.Repeat(" ", leftPad)
+ return WrapWithPad(text, lineWidth, pad)
+}
+
+// WrapWithPad wrap a text for a given line size with a custom left padding
+// Handle properly terminal color escape code
+func WrapWithPad(text string, lineWidth int, pad string) (string, int) {
+ return WrapWithPadIndent(text, lineWidth, pad, pad)
+}
+
+// WrapWithPad wrap a text for a given line size with a custom left padding
+// This function also align the result depending on the requested alignment.
+// Handle properly terminal color escape code
+func WrapWithPadAlign(text string, lineWidth int, pad string, align Alignment) (string, int) {
+ return WrapWithPadIndentAlign(text, lineWidth, pad, pad, align)
+}
+
+// WrapWithPadIndent wrap a text for a given line size with a custom left padding
+// and a first line indent. The padding is not effective on the first line, indent
+// is used instead, which allow to implement indents and outdents.
+// Handle properly terminal color escape code
+func WrapWithPadIndent(text string, lineWidth int, indent string, pad string) (string, int) {
+ return WrapWithPadIndentAlign(text, lineWidth, indent, pad, NoAlign)
+}
+
+// WrapWithPadIndentAlign wrap a text for a given line size with a custom left padding
+// and a first line indent. The padding is not effective on the first line, indent
+// is used instead, which allow to implement indents and outdents.
+// This function also align the result depending on the requested alignment.
+// Handle properly terminal color escape code
+func WrapWithPadIndentAlign(text string, lineWidth int, indent string, pad string, align Alignment) (string, int) {
+ var lines []string
+ nbLine := 0
+
+ // Start with the indent
+ padStr := indent
+ padLen := Len(indent)
+
+ // tabs are formatted as 4 spaces
+ text = strings.Replace(text, "\t", " ", -1)
+
+ // NOTE: text is first segmented into lines so that softwrapLine can handle.
+ for i, line := range strings.Split(text, "\n") {
+ // on the second line, use the padding instead
+ if i == 1 {
+ padStr = pad
+ padLen = Len(pad)
+ }
+
+ if line == "" || strings.TrimSpace(line) == "" {
+ // nothing in the line, we just add the non-empty part of the padding
+ lines = append(lines, strings.TrimRight(padStr, " "))
+ nbLine++
+ continue
+ }
+
+ wrapped := softwrapLine(line, lineWidth-padLen)
+ split := strings.Split(wrapped, "\n")
+
+ if i == 0 && len(split) > 1 {
+ // the very first line got wrapped
+ // that means we need to switch to the normal padding
+ // use the first wrapped line, ignore everything else and
+ // wrap the remaining of the line with the normal padding.
+
+ content := LineAlign(strings.TrimRight(split[0], " "), lineWidth-padLen, align)
+ lines = append(lines, padStr+content)
+ nbLine++
+ line = strings.TrimPrefix(line, split[0])
+ line = strings.TrimLeft(line, " ")
+
+ padStr = pad
+ padLen = Len(pad)
+ wrapped = softwrapLine(line, lineWidth-padLen)
+ split = strings.Split(wrapped, "\n")
+ }
+
+ for j, seg := range split {
+ if j == 0 {
+ // keep the left padding of the wrapped line
+ content := LineAlign(strings.TrimRight(seg, " "), lineWidth-padLen, align)
+ lines = append(lines, padStr+content)
+ } else {
+ content := LineAlign(strings.TrimSpace(seg), lineWidth-padLen, align)
+ lines = append(lines, padStr+content)
+ }
+ nbLine++
+ }
+ }
+
+ return strings.Join(lines, "\n"), nbLine
+}
+
+// Break a line into several lines so that each line consumes at most
+// 'textWidth' cells. Lines break at groups of white spaces and multibyte
+// chars. Nothing is removed from the original text so that it behaves like a
+// softwrap.
+//
+// Required: The line shall not contain '\n'
+//
+// WRAPPING ALGORITHM: The line is broken into non-breakable chunks, then line
+// breaks ("\n") are inserted between these groups so that the total length
+// between breaks does not exceed the required width. Words that are longer than
+// the textWidth are broken into pieces no longer than textWidth.
+func softwrapLine(line string, textWidth int) string {
+ escaped, escapes := ExtractTermEscapes(line)
+
+ chunks := segmentLine(escaped)
+ // Reverse the chunk array so we can use it as a stack.
+ for i, j := 0, len(chunks)-1; i < j; i, j = i+1, j-1 {
+ chunks[i], chunks[j] = chunks[j], chunks[i]
+ }
+
+ // for readability, minimal implementation of a stack:
+
+ pop := func() string {
+ result := chunks[len(chunks)-1]
+ chunks = chunks[:len(chunks)-1]
+ return result
+ }
+
+ push := func(chunk string) {
+ chunks = append(chunks, chunk)
+ }
+
+ peek := func() string {
+ return chunks[len(chunks)-1]
+ }
+
+ empty := func() bool {
+ return len(chunks) == 0
+ }
+
+ var out strings.Builder
+
+ // helper to write in the output while interleaving the escape
+ // sequence at the correct places.
+ // note: the final algorithm will add additional line break in the original
+ // text. Those line break are *not* fed to this helper so the positions don't
+ // need to be offset, which make the whole thing much easier.
+ currPos := 0
+ currItem := 0
+ outputString := func(s string) {
+ for _, r := range s {
+ for currItem < len(escapes) && currPos == escapes[currItem].Pos {
+ out.WriteString(escapes[currItem].Item)
+ currItem++
+ }
+ out.WriteRune(r)
+ currPos++
+ }
+ }
+
+ width := 0
+
+ for !empty() {
+ wl := Len(peek())
+
+ if width+wl <= textWidth {
+ // the chunk fit in the available space
+ outputString(pop())
+ width += wl
+ if width == textWidth && !empty() {
+ // only add line break when there is more chunk to come
+ out.WriteRune('\n')
+ width = 0
+ }
+ } else if wl > textWidth {
+ // words too long for a full line are split to fill the remaining space.
+ // But if the long words is the first non-space word in the middle of the
+ // line, preceding spaces shall not be counted in word splitting.
+ splitWidth := textWidth - width
+ if strings.HasSuffix(out.String(), "\n"+strings.Repeat(" ", width)) {
+ splitWidth += width
+ }
+ left, right := splitWord(pop(), splitWidth)
+ // remainder is pushed back to the stack for next round
+ push(right)
+ outputString(left)
+ out.WriteRune('\n')
+ width = 0
+ } else {
+ // normal line overflow, we add a line break and try again
+ out.WriteRune('\n')
+ width = 0
+ }
+ }
+
+ // Don't forget the trailing escapes, if any.
+ for currItem < len(escapes) && currPos >= escapes[currItem].Pos {
+ out.WriteString(escapes[currItem].Item)
+ currItem++
+ }
+
+ return out.String()
+}
+
+// Segment a line into chunks, where each chunk consists of chars with the same
+// type and is not breakable.
+func segmentLine(s string) []string {
+ var chunks []string
+
+ var word string
+ wordType := none
+ flushWord := func() {
+ chunks = append(chunks, word)
+ word = ""
+ wordType = none
+ }
+
+ for _, r := range s {
+ // A WIDE_CHAR itself constitutes a chunk.
+ thisType := runeType(r)
+ if thisType == wideChar {
+ if wordType != none {
+ flushWord()
+ }
+ chunks = append(chunks, string(r))
+ continue
+ }
+ // Other type of chunks starts with a char of that type, and ends with a
+ // char with different type or end of string.
+ if thisType != wordType {
+ if wordType != none {
+ flushWord()
+ }
+ word = string(r)
+ wordType = thisType
+ } else {
+ word += string(r)
+ }
+ }
+ if word != "" {
+ flushWord()
+ }
+
+ return chunks
+}
+
+type RuneType int
+
+// Rune categories
+//
+// These categories are so defined that each category forms a non-breakable
+// chunk. It IS NOT the same as unicode code point categories.
+const (
+ none RuneType = iota
+ wideChar
+ invisible
+ shortUnicode
+ space
+ visibleAscii
+)
+
+// Determine the category of a rune.
+func runeType(r rune) RuneType {
+ rw := runewidth.RuneWidth(r)
+ if rw > 1 {
+ return wideChar
+ } else if rw == 0 {
+ return invisible
+ } else if r > 127 {
+ return shortUnicode
+ } else if r == ' ' {
+ return space
+ } else {
+ return visibleAscii
+ }
+}
+
+// splitWord split a word at the given length, while ignoring the terminal escape sequences
+func splitWord(word string, length int) (string, string) {
+ runes := []rune(word)
+ var result []rune
+ added := 0
+ escape := false
+
+ if length == 0 {
+ return "", word
+ }
+
+ for _, r := range runes {
+ if r == '\x1b' {
+ escape = true
+ }
+
+ width := runewidth.RuneWidth(r)
+ if width+added > length {
+ // wide character made the length overflow
+ break
+ }
+
+ result = append(result, r)
+
+ if !escape {
+ added += width
+ if added >= length {
+ break
+ }
+ }
+
+ if r == 'm' {
+ escape = false
+ }
+ }
+
+ leftover := runes[len(result):]
+
+ return string(result), string(leftover)
+}