switch to go-term-text to fix bad underflow for label rendering

Michael Muré created

Change summary

Gopkg.lock                                             |  10 
Gopkg.toml                                             |   4 
commands/bridge.go                                     |   3 
commands/comment.go                                    |   7 
commands/label_rm.go                                   |   3 
commands/ls.go                                         |  25 
commands/select.go                                     |   3 
termui/bug_table.go                                    |  32 
termui/label_select.go                                 |   3 
termui/msg_popup.go                                    |   2 
termui/show_bug.go                                     |   5 
util/text/left_padded.go                               |  42 -
util/text/left_padded_test.go                          |  56 -
util/text/text.go                                      | 330 ----------
util/text/text_test.go                                 | 376 ------------
vendor/github.com/MichaelMure/go-term-text/.gitignore  |   1 
vendor/github.com/MichaelMure/go-term-text/.travis.yml |  16 
vendor/github.com/MichaelMure/go-term-text/LICENSE     |  21 
vendor/github.com/MichaelMure/go-term-text/Readme.md   |  71 ++
vendor/github.com/MichaelMure/go-term-text/align.go    |  67 ++
vendor/github.com/MichaelMure/go-term-text/escapes.go  |  95 +++
vendor/github.com/MichaelMure/go-term-text/go.mod      |   8 
vendor/github.com/MichaelMure/go-term-text/go.sum      |   9 
vendor/github.com/MichaelMure/go-term-text/left_pad.go |  50 +
vendor/github.com/MichaelMure/go-term-text/len.go      |  45 +
vendor/github.com/MichaelMure/go-term-text/trim.go     |  28 
vendor/github.com/MichaelMure/go-term-text/truncate.go |  24 
vendor/github.com/MichaelMure/go-term-text/wrap.go     | 334 ++++++++++
28 files changed, 826 insertions(+), 844 deletions(-)

Detailed changes

Gopkg.lock 🔗

@@ -24,6 +24,14 @@
   revision = "4eeacc6e4cb7bedc7c5312b6a3947697ad5cfb55"
   version = "v0.9.2"
 
+[[projects]]
+  digest = "1:b46ef47d5fcc120e6fc1f75e75106f31cbb51fe9981234b5c191d0083d8f9867"
+  name = "github.com/MichaelMure/go-term-text"
+  packages = ["."]
+  pruneopts = "UT"
+  revision = "60f9049b9d18b9370b8ed1247fe4334af5db131a"
+  version = "v0.2.1"
+
 [[projects]]
   branch = "master"
   digest = "1:38a84d9b4cf50b3e8eb2b54f218413ac163076e3a7763afe5fa15a4eb15fbda6"
@@ -464,6 +472,7 @@
     "github.com/99designs/gqlgen/graphql",
     "github.com/99designs/gqlgen/graphql/introspection",
     "github.com/99designs/gqlgen/handler",
+    "github.com/MichaelMure/go-term-text",
     "github.com/MichaelMure/gocui",
     "github.com/blang/semver",
     "github.com/cheekybits/genny/generic",
@@ -471,7 +480,6 @@
     "github.com/fatih/color",
     "github.com/gorilla/mux",
     "github.com/icrowley/fake",
-    "github.com/mattn/go-runewidth",
     "github.com/phayes/freeport",
     "github.com/pkg/errors",
     "github.com/shurcooL/githubv4",

Gopkg.toml 🔗

@@ -79,3 +79,7 @@
 [[constraint]]
   branch = "master"
   name = "golang.org/x/sync"
+
+[[constraint]]
+  name = "github.com/MichaelMure/go-term-text"
+  version = "0.2.1"

commands/bridge.go 🔗

@@ -3,10 +3,11 @@ package commands
 import (
 	"fmt"
 
+	"github.com/spf13/cobra"
+
 	"github.com/MichaelMure/git-bug/bridge"
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/util/interrupt"
-	"github.com/spf13/cobra"
 )
 
 func runBridge(cmd *cobra.Command, args []string) error {

commands/comment.go 🔗

@@ -3,13 +3,14 @@ package commands
 import (
 	"fmt"
 
+	"github.com/MichaelMure/go-term-text"
+	"github.com/spf13/cobra"
+
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/commands/select"
 	"github.com/MichaelMure/git-bug/util/colors"
 	"github.com/MichaelMure/git-bug/util/interrupt"
-	"github.com/MichaelMure/git-bug/util/text"
-	"github.com/spf13/cobra"
 )
 
 func runComment(cmd *cobra.Command, args []string) error {
@@ -41,7 +42,7 @@ func commentsTextOutput(comments []bug.Comment) {
 		fmt.Printf("Author: %s\n", colors.Magenta(comment.Author.DisplayName()))
 		fmt.Printf("Id: %s\n", colors.Cyan(comment.Id().Human()))
 		fmt.Printf("Date: %s\n\n", comment.FormatTime())
-		fmt.Println(text.LeftPad(comment.Message, 4))
+		fmt.Println(text.LeftPadLines(comment.Message, 4))
 	}
 }
 

commands/label_rm.go 🔗

@@ -3,10 +3,11 @@ package commands
 import (
 	"fmt"
 
+	"github.com/spf13/cobra"
+
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/commands/select"
 	"github.com/MichaelMure/git-bug/util/interrupt"
-	"github.com/spf13/cobra"
 )
 
 func runLabelRm(cmd *cobra.Command, args []string) error {

commands/ls.go 🔗

@@ -4,11 +4,12 @@ import (
 	"fmt"
 	"strings"
 
+	text "github.com/MichaelMure/go-term-text"
+	"github.com/spf13/cobra"
+
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/util/colors"
 	"github.com/MichaelMure/git-bug/util/interrupt"
-	"github.com/MichaelMure/git-bug/util/text"
-	"github.com/spf13/cobra"
 )
 
 var (
@@ -65,21 +66,17 @@ func runLsBug(cmd *cobra.Command, args []string) error {
 			name = b.LegacyAuthor.DisplayName()
 		}
 
-		labelsTxt := ""
-		nbLabels := 0
+		var labelsTxt strings.Builder
 		for _, l := range b.Labels {
-			lc := l.Color()
-			lc256 := lc.Term256()
-			nbLabels++
-			if nbLabels >= 5 && len(b.Labels) > 5 {
-				labelsTxt += " …"
-				break
-			}
-			labelsTxt += lc256.Escape() + " ◼" + lc256.Unescape()
+			lc256 := l.Color().Term256()
+			labelsTxt.WriteString(lc256.Escape())
+			labelsTxt.WriteString(" ◼")
+			labelsTxt.WriteString(lc256.Unescape())
 		}
 
 		// truncate + pad if needed
-		titleFmt := text.LeftPadMaxLine(b.Title, 50-(nbLabels*2), 0)
+		labelsFmt := text.TruncateMax(labelsTxt.String(), 10)
+		titleFmt := text.LeftPadMaxLine(b.Title, 50-text.Len(labelsFmt), 0)
 		authorFmt := text.LeftPadMaxLine(name, 15, 0)
 
 		comments := fmt.Sprintf("%4d 💬", b.LenComments)
@@ -90,7 +87,7 @@ func runLsBug(cmd *cobra.Command, args []string) error {
 		fmt.Printf("%s %s\t%s\t%s\t%s\n",
 			colors.Cyan(b.Id.Human()),
 			colors.Yellow(b.Status),
-			titleFmt+labelsTxt,
+			titleFmt+labelsFmt,
 			colors.Magenta(authorFmt),
 			comments,
 		)

commands/select.go 🔗

@@ -4,10 +4,11 @@ import (
 	"errors"
 	"fmt"
 
+	"github.com/spf13/cobra"
+
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/commands/select"
 	"github.com/MichaelMure/git-bug/util/interrupt"
-	"github.com/spf13/cobra"
 )
 
 func runSelect(cmd *cobra.Command, args []string) error {

termui/bug_table.go 🔗

@@ -3,14 +3,16 @@ package termui
 import (
 	"bytes"
 	"fmt"
+	"strings"
 	"time"
 
+	"github.com/MichaelMure/go-term-text"
+	"github.com/MichaelMure/gocui"
+	"github.com/dustin/go-humanize"
+
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/util/colors"
-	"github.com/MichaelMure/git-bug/util/text"
-	"github.com/MichaelMure/gocui"
-	"github.com/dustin/go-humanize"
 )
 
 const bugTableView = "bugTableView"
@@ -291,21 +293,19 @@ func (bt *bugTable) render(v *gocui.View, maxX int) {
 
 	for _, excerpt := range bt.excerpts {
 		summaryTxt := fmt.Sprintf("%4d 💬", excerpt.LenComments)
+		if excerpt.LenComments <= 0 {
+			summaryTxt = ""
+		}
 		if excerpt.LenComments > 9999 {
 			summaryTxt = "    ∞ 💬"
 		}
-		labelsTxt := ""
 
-		nbLabels := 0
+		var labelsTxt strings.Builder
 		for _, l := range excerpt.Labels {
-			lc := l.Color()
-			lc256 := lc.Term256()
-			nbLabels++
-			if nbLabels >= 5 && len(excerpt.Labels) > 5 {
-				labelsTxt += " …"
-				break
-			}
-			labelsTxt += lc256.Escape() + " ◼" + lc256.Unescape()
+			lc256 := l.Color().Term256()
+			labelsTxt.WriteString(lc256.Escape())
+			labelsTxt.WriteString(" ◼")
+			labelsTxt.WriteString(lc256.Unescape())
 		}
 
 		var authorDisplayName string
@@ -323,15 +323,17 @@ func (bt *bugTable) render(v *gocui.View, maxX int) {
 
 		id := text.LeftPadMaxLine(excerpt.Id.Human(), columnWidths["id"], 1)
 		status := text.LeftPadMaxLine(excerpt.Status.String(), columnWidths["status"], 1)
-		title := text.LeftPadMaxLine(excerpt.Title, columnWidths["title"]-(nbLabels*2), 1) + labelsTxt
+		labels := text.TruncateMax(labelsTxt.String(), minInt(columnWidths["title"]-2, 10))
+		title := text.LeftPadMaxLine(excerpt.Title, columnWidths["title"]-text.Len(labels), 1)
 		author := text.LeftPadMaxLine(authorDisplayName, columnWidths["author"], 1)
 		comments := text.LeftPadMaxLine(summaryTxt, columnWidths["comments"], 1)
 		lastEdit := text.LeftPadMaxLine(humanize.Time(lastEditTime), columnWidths["lastEdit"], 1)
 
-		_, _ = fmt.Fprintf(v, "%s %s %s %s %s %s\n",
+		_, _ = fmt.Fprintf(v, "%s %s %s%s %s %s %s\n",
 			colors.Cyan(id),
 			colors.Yellow(status),
 			title,
+			labels,
 			colors.Magenta(author),
 			comments,
 			lastEdit,

termui/label_select.go 🔗

@@ -4,9 +4,10 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/MichaelMure/gocui"
+
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/cache"
-	"github.com/MichaelMure/gocui"
 )
 
 const labelSelectView = "labelSelectView"

termui/msg_popup.go 🔗

@@ -3,7 +3,7 @@ package termui
 import (
 	"fmt"
 
-	"github.com/MichaelMure/git-bug/util/text"
+	"github.com/MichaelMure/go-term-text"
 	"github.com/MichaelMure/gocui"
 )
 

termui/show_bug.go 🔗

@@ -5,12 +5,13 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/MichaelMure/go-term-text"
+	"github.com/MichaelMure/gocui"
+
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/util/colors"
-	"github.com/MichaelMure/git-bug/util/text"
-	"github.com/MichaelMure/gocui"
 )
 
 const showBugView = "showBugView"

util/text/left_padded.go 🔗

@@ -1,42 +0,0 @@
-package text
-
-import (
-	"bytes"
-	"fmt"
-	"github.com/mattn/go-runewidth"
-	"strings"
-)
-
-// LeftPadMaxLine pads a string on the left by a specified amount and pads the
-// string on the right to fill the maxLength
-func LeftPadMaxLine(text string, length, leftPad int) string {
-	var rightPart string = text
-
-	scrWidth := runewidth.StringWidth(text)
-	// truncate and ellipse if needed
-	if scrWidth+leftPad > length {
-		rightPart = runewidth.Truncate(text, length-leftPad, "…")
-	} else if scrWidth+leftPad < length {
-		rightPart = runewidth.FillRight(text, length-leftPad)
-	}
-
-	return fmt.Sprintf("%s%s",
-		strings.Repeat(" ", leftPad),
-		rightPart,
-	)
-}
-
-// LeftPad left pad each line of the given text
-func LeftPad(text string, leftPad int) string {
-	var result bytes.Buffer
-
-	pad := strings.Repeat(" ", leftPad)
-
-	for _, line := range strings.Split(text, "\n") {
-		result.WriteString(pad)
-		result.WriteString(line)
-		result.WriteString("\n")
-	}
-
-	return result.String()
-}

util/text/left_padded_test.go 🔗

@@ -1,56 +0,0 @@
-package text
-
-import "testing"
-
-func TestLeftPadMaxLine(t *testing.T) {
-	cases := []struct {
-		input, output  string
-		maxValueLength int
-		leftPad        int
-	}{
-		{
-			"foo",
-			"foo ",
-			4,
-			0,
-		},
-		{
-			"foofoofoo",
-			"foo…",
-			4,
-			0,
-		},
-		{
-			"foo",
-			"foo       ",
-			10,
-			0,
-		},
-		{
-			"foo",
-			"  f…",
-			4,
-			2,
-		},
-		{
-			"foofoofoo",
-			"  foo…",
-			6,
-			2,
-		},
-		{
-			"foo",
-			"  foo     ",
-			10,
-			2,
-		},
-	}
-
-	for i, tc := range cases {
-		result := LeftPadMaxLine(tc.input, tc.maxValueLength, tc.leftPad)
-		if result != tc.output {
-			t.Fatalf("Case %d Input:\n\n`%s`\n\nExpected Output:\n\n`%s`\n\nActual Output:\n\n`%s`",
-				i, tc.input, tc.output, result)
-		}
-	}
-}

util/text/text.go 🔗

@@ -1,330 +0,0 @@
-package text
-
-import (
-	"github.com/mattn/go-runewidth"
-	"strings"
-	"unicode/utf8"
-)
-
-// 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 an exact line size
-// Handle properly terminal color escape code
-func Wrap(text string, lineWidth int) (string, int) {
-	return WrapLeftPadded(text, lineWidth, 0)
-}
-
-// Wrap a text for an exact line size with a left padding
-// Handle properly terminal color escape code
-func WrapLeftPadded(text string, lineWidth int, leftPad int) (string, int) {
-	var lines []string
-	nbLine := 0
-	pad := strings.Repeat(" ", leftPad)
-
-	// 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 _, line := range strings.Split(text, "\n") {
-		if line == "" || strings.TrimSpace(line) == "" {
-			lines = append(lines, "")
-			nbLine++
-		} else {
-			wrapped := softwrapLine(line, lineWidth-leftPad)
-			firstLine := true
-			for _, seg := range strings.Split(wrapped, "\n") {
-				if firstLine {
-					lines = append(lines, pad+strings.TrimRight(seg, " "))
-					firstLine = false
-				} else {
-					lines = append(lines, pad+strings.TrimSpace(seg))
-				}
-				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 {
-	// NOTE: terminal escapes are stripped out of the line so the algorithm is
-	// simpler. Do not try to mix them in the wrapping algorithm, as it can get
-	// complicated quickly.
-	line1, termEscapes := extractTermEscapes(line)
-
-	chunks := segmentLine(line1)
-	// 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]
-	}
-	var line2 string = ""
-	var width int = 0
-	for len(chunks) > 0 {
-		thisWord := chunks[len(chunks)-1]
-		wl := wordLen(thisWord)
-		if width+wl <= textWidth {
-			line2 += chunks[len(chunks)-1]
-			chunks = chunks[:len(chunks)-1]
-			width += wl
-			if width == textWidth && len(chunks) > 0 {
-				// NOTE: new line begins when current line is full and there are more
-				// chunks to come.
-				line2 += "\n"
-				width = 0
-			}
-		} else if wl > textWidth {
-			// NOTE: By default, long words are splited to fill the remaining space.
-			// But if the long words is the first non-space word in the middle of the
-			// line, preceeding spaces shall not be counted in word spliting.
-			splitWidth := textWidth - width
-			if strings.HasSuffix(line2, "\n"+strings.Repeat(" ", width)) {
-				splitWidth += width
-			}
-			left, right := splitWord(chunks[len(chunks)-1], splitWidth)
-			chunks[len(chunks)-1] = right
-			line2 += left + "\n"
-			width = 0
-		} else {
-			line2 += "\n"
-			width = 0
-		}
-	}
-
-	line3 := applyTermEscapes(line2, termEscapes)
-	return line3
-}
-
-// EscapeItem: Storage of terminal escapes in a line. 'item' is the actural
-// escape command, and '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
-}
-
-// Extract terminal escapes out of a line, 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 string
-
-	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 += string(r)
-	}
-
-	return line1, termEscapes
-}
-
-// Apply the extracted terminal escapes to the edited line. The only edit
-// allowed is to insert "\n" like that in softwrapLine. Callers shall ensure
-// this since this function is not able to check it.
-func applyTermEscapes(line string, escapes []escapeItem) string {
-	if len(escapes) == 0 {
-		return line
-	}
-
-	var out string = ""
-
-	currPos := 0
-	currItem := 0
-	for _, r := range line {
-		if currItem < len(escapes) && currPos == escapes[currItem].pos {
-			// NOTE: We avoid terminal escapes at the end of a line by move them one
-			// pass the end of line, so that algorithms who trim right spaces are
-			// happy. But algorithms who trim left spaces are still unhappy.
-			if r == '\n' {
-				out += "\n" + escapes[currItem].item
-			} else {
-				out += escapes[currItem].item + string(r)
-				currPos++
-			}
-			currItem++
-		} else {
-			if r != '\n' {
-				currPos++
-			}
-			out += string(r)
-		}
-	}
-
-	// Don't forget the trailing escape, if any.
-	if currItem == len(escapes)-1 && currPos == escapes[currItem].pos {
-		out += escapes[currItem].item
-	}
-
-	return out
-}
-
-// 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
-}
-
-// 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 int = iota
-	wideChar
-	invisible
-	shortUnicode
-	space
-	visibleAscii
-)
-
-// Determine the category of a rune.
-func runeType(r rune) int {
-	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
-	}
-}
-
-// wordLen return the length of a word, while ignoring the terminal escape
-// sequences
-func wordLen(word string) int {
-	length := 0
-	escape := false
-
-	for _, char := range word {
-		if char == '\x1b' {
-			escape = true
-		}
-		if !escape {
-			length += runewidth.RuneWidth(rune(char))
-		}
-		if char == 'm' {
-			escape = false
-		}
-	}
-
-	return length
-}
-
-// 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)
-}

util/text/text_test.go 🔗

@@ -1,376 +0,0 @@
-package text
-
-import (
-	"reflect"
-	"strings"
-	"testing"
-)
-
-func TestWrap(t *testing.T) {
-	cases := []struct {
-		Input, Output string
-		Lim           int
-	}{
-		// A simple word passes through.
-		{
-			"foo",
-			"foo",
-			4,
-		},
-		// Word breaking
-		{
-			"foobarbaz",
-			"foob\narba\nz",
-			4,
-		},
-		// Lines are broken at whitespace.
-		{
-			"foo bar baz",
-			"foo\nbar\nbaz",
-			4,
-		},
-		// Word breaking
-		{
-			"foo bars bazzes",
-			"foo\nbars\nbazz\nes",
-			4,
-		},
-		// A word that would run beyond the width is wrapped.
-		{
-			"fo sop",
-			"fo\nsop",
-			4,
-		},
-		// A tab counts as 4 characters.
-		{
-			"foo\nb\t r\n baz",
-			"foo\nb\nr\n baz",
-			4,
-		},
-		// Trailing whitespace is removed after used for wrapping.
-		// Runs of whitespace on which a line is broken are removed.
-		{
-			"foo    \nb   ar   ",
-			"foo\n\nb\nar\n",
-			4,
-		},
-		// An explicit line break at the end of the input is preserved.
-		{
-			"foo bar baz\n",
-			"foo\nbar\nbaz\n",
-			4,
-		},
-		// Explicit break are always preserved.
-		{
-			"\nfoo bar\n\n\nbaz\n",
-			"\nfoo\nbar\n\n\nbaz\n",
-			4,
-		},
-		// Ignore complete words with terminal color sequence
-		{
-			"foo \x1b[31mbar\x1b[0m baz",
-			"foo\n\x1b[31mbar\x1b[0m\nbaz",
-			4,
-		},
-		// Handle words with colors sequence inside the word
-		{
-			"foo b\x1b[31mbar\x1b[0mr baz",
-			"foo\nb\x1b[31mbar\n\x1b[0mr\nbaz",
-			4,
-		},
-		// Break words with colors sequence inside the word
-		{
-			"foo bb\x1b[31mbar\x1b[0mr baz",
-			"foo\nbb\x1b[31mba\nr\x1b[0mr\nbaz",
-			4,
-		},
-		// Complete example:
-		{
-			" This is a list: \n\n\t* foo\n\t* bar\n\n\n\t* baz  \nBAM    ",
-			" This\nis a\nlist:\n\n    *\nfoo\n    *\nbar\n\n\n    *\nbaz\nBAM\n",
-			6,
-		},
-		// Handle chinese (wide characters)
-		{
-			"一只敏捷的狐狸跳过了一只懒狗。",
-			"一只敏捷的狐\n狸跳过了一只\n懒狗。",
-			12,
-		},
-		// Handle chinese with colors
-		{
-			"一只敏捷的\x1b[31m狐狸跳过\x1b[0m了一只懒狗。",
-			"一只敏捷的\x1b[31m狐\n狸跳过\x1b[0m了一只\n懒狗。",
-			12,
-		},
-		// Handle mixed wide and short characters
-		{
-			"敏捷 A quick 的狐狸 fox 跳过 jumps over a lazy 了一只懒狗 dog。",
-			"敏捷 A quick\n的狐狸 fox\n跳过 jumps\nover a lazy\n了一只懒狗\ndog。",
-			12,
-		},
-		// Handle mixed wide and short characters with color
-		{
-			"敏捷 A \x1b31mquick 的狐狸 fox 跳\x1b0m过 jumps over a lazy 了一只懒狗 dog。",
-			"敏捷 A \x1b31mquick\n的狐狸 fox\n跳\x1b0m过 jumps\nover a lazy\n了一只懒狗\ndog。",
-			12,
-		},
-	}
-
-	for i, tc := range cases {
-		actual, lines := Wrap(tc.Input, tc.Lim)
-		if actual != tc.Output {
-			t.Fatalf("Case %d Input:\n\n`%s`\n\nExpected Output:\n\n`%s`\n\nActual Output:\n\n`%s`",
-				i, tc.Input, tc.Output, actual)
-		}
-
-		expected := len(strings.Split(tc.Output, "\n"))
-		if expected != lines {
-			t.Fatalf("Case %d Nb lines mismatch\nExpected:%d\nActual:%d",
-				i, expected, lines)
-		}
-	}
-}
-
-func TestWrapLeftPadded(t *testing.T) {
-	cases := []struct {
-		input, output string
-		lim, pad      int
-	}{
-		{
-			"The Lorem ipsum text is typically composed of pseudo-Latin words. It is commonly used as placeholder text to examine or demonstrate the visual effects of various graphic design.",
-			`    The Lorem ipsum text is typically composed of
-    pseudo-Latin words. It is commonly used as placeholder
-    text to examine or demonstrate the visual effects of
-    various graphic design.`,
-			59, 4,
-		},
-		// Handle Chinese
-		{
-			"婞一枳郲逴靲屮蜧曀殳,掫乇峔掮傎溒兀緉冘仜。郼牪艽螗媷錵朸一詅掜豗怙刉笀丌,楀棶乇矹迡搦囷圣亍昄漚粁仈祂。覂一洳袶揙楱亍滻瘯毌,掗屮柅軡菵腩乜榵毌夯。勼哻怌婇怤灟葠雺奷朾恦扰衪岨坋誁乇芚誙腞。冇笉妺悆浂鱦賌廌灱灱觓坋佫呬耴跣兀枔蓔輈。嵅咍犴膰痭瘰机一靬涽捊矷尒玶乇,煚塈丌岰陊鉖怞戉兀甿跾觓夬侄。棩岧汌橩僁螗玎一逭舴圂衪扐衲兀,嵲媕亍衩衿溽昃夯丌侄蒰扂丱呤。毰侘妅錣廇螉仴一暀淖蚗佶庂咺丌,輀鈁乇彽洢溦洰氶乇构碨洐巿阹。",
-			`    婞一枳郲逴靲屮蜧曀殳,掫乇峔掮傎溒兀緉冘仜。郼牪艽螗媷
-    錵朸一詅掜豗怙刉笀丌,楀棶乇矹迡搦囷圣亍昄漚粁仈祂。覂
-    一洳袶揙楱亍滻瘯毌,掗屮柅軡菵腩乜榵毌夯。勼哻怌婇怤灟
-    葠雺奷朾恦扰衪岨坋誁乇芚誙腞。冇笉妺悆浂鱦賌廌灱灱觓坋
-    佫呬耴跣兀枔蓔輈。嵅咍犴膰痭瘰机一靬涽捊矷尒玶乇,煚塈
-    丌岰陊鉖怞戉兀甿跾觓夬侄。棩岧汌橩僁螗玎一逭舴圂衪扐衲
-    兀,嵲媕亍衩衿溽昃夯丌侄蒰扂丱呤。毰侘妅錣廇螉仴一暀淖
-    蚗佶庂咺丌,輀鈁乇彽洢溦洰氶乇构碨洐巿阹。`,
-			59, 4,
-		},
-		// Handle long unbreakable words in a full stentence
-		{
-			"OT: there are alternatives to maintainer-/user-set priority, e.g. \"[user pain](http://www.lostgarden.com/2008/05/improving-bug-triage-with-user-pain.html)\".",
-			`    OT: there are alternatives to maintainer-/user-set
-    priority, e.g. "[user pain](http://www.lostgarden.com/
-    2008/05/improving-bug-triage-with-user-pain.html)".`,
-			58, 4,
-		},
-	}
-
-	for i, tc := range cases {
-		actual, lines := WrapLeftPadded(tc.input, tc.lim, tc.pad)
-		if actual != tc.output {
-			t.Fatalf("Case %d Input:\n\n`%s`\n\nExpected Output:\n`\n%s`\n\nActual Output:\n`\n%s\n%s`",
-				i, tc.input, tc.output,
-				"|"+strings.Repeat("-", tc.lim-2)+"|",
-				actual)
-		}
-
-		expected := len(strings.Split(tc.output, "\n"))
-		if expected != lines {
-			t.Fatalf("Case %d Nb lines mismatch\nExpected:%d\nActual:%d",
-				i, expected, lines)
-		}
-	}
-}
-
-func TestWordLen(t *testing.T) {
-	cases := []struct {
-		Input  string
-		Length int
-	}{
-		// A simple word
-		{
-			"foo",
-			3,
-		},
-		// A simple word with colors
-		{
-			"\x1b[31mbar\x1b[0m",
-			3,
-		},
-		// Handle prefix and suffix properly
-		{
-			"foo\x1b[31mfoobarHoy\x1b[0mbaaar",
-			17,
-		},
-		// Handle chinese
-		{
-			"快檢什麼望對",
-			12,
-		},
-		// Handle chinese with colors
-		{
-			"快\x1b[31m檢什麼\x1b[0m望對",
-			12,
-		},
-	}
-
-	for i, tc := range cases {
-		l := wordLen(tc.Input)
-		if l != tc.Length {
-			t.Fatalf("Case %d Input:\n\n`%s`\n\nExpected Output:\n\n`%d`\n\nActual Output:\n\n`%d`",
-				i, tc.Input, tc.Length, l)
-		}
-	}
-}
-
-func TestSplitWord(t *testing.T) {
-	cases := []struct {
-		Input            string
-		Length           int
-		Result, Leftover string
-	}{
-		// A simple word passes through.
-		{
-			"foo",
-			4,
-			"foo", "",
-		},
-		// Cut at the right place
-		{
-			"foobarHoy",
-			4,
-			"foob", "arHoy",
-		},
-		// A simple word passes through with colors
-		{
-			"\x1b[31mbar\x1b[0m",
-			4,
-			"\x1b[31mbar\x1b[0m", "",
-		},
-		// Cut at the right place with colors
-		{
-			"\x1b[31mfoobarHoy\x1b[0m",
-			4,
-			"\x1b[31mfoob", "arHoy\x1b[0m",
-		},
-		// Handle prefix and suffix properly
-		{
-			"foo\x1b[31mfoobarHoy\x1b[0mbaaar",
-			4,
-			"foo\x1b[31mf", "oobarHoy\x1b[0mbaaar",
-		},
-		// Cut properly with length = 0
-		{
-			"foo",
-			0,
-			"", "foo",
-		},
-		// Handle chinese
-		{
-			"快檢什麼望對",
-			4,
-			"快檢", "什麼望對",
-		},
-		{
-			"快檢什麼望對",
-			5,
-			"快檢", "什麼望對",
-		},
-		// Handle chinese with colors
-		{
-			"快\x1b[31m檢什麼\x1b[0m望對",
-			4,
-			"快\x1b[31m檢", "什麼\x1b[0m望對",
-		},
-	}
-
-	for i, tc := range cases {
-		result, leftover := splitWord(tc.Input, tc.Length)
-		if result != tc.Result || leftover != tc.Leftover {
-			t.Fatalf("Case %d Input:\n\n`%s`\n\nExpected Output:\n\n`%s` - `%s`\n\nActual Output:\n\n`%s` - `%s`",
-				i, tc.Input, tc.Result, tc.Leftover, result, leftover)
-		}
-	}
-}
-
-func TestExtractApplyTermEscapes(t *testing.T) {
-	cases := []struct {
-		Input       string
-		Output      string
-		TermEscapes []escapeItem
-	}{
-		// A plain ascii line with escapes.
-		{
-			"This \x1b[31mis an\x1b[0m example.",
-			"This is an example.",
-			[]escapeItem{{"\x1b[31m", 5}, {"\x1b[0m", 10}},
-		},
-		// Escape at the end
-		{
-			"This \x1b[31mis an example.\x1b[0m",
-			"This is an example.",
-			[]escapeItem{{"\x1b[31m", 5}, {"\x1b[0m", 19}},
-		},
-		// A plain wide line with escapes.
-		{
-			"一只敏捷\x1b[31m的狐狸\x1b[0m跳过了一只懒狗。",
-			"一只敏捷的狐狸跳过了一只懒狗。",
-			[]escapeItem{{"\x1b[31m", 4}, {"\x1b[0m", 7}},
-		},
-		// A normal-wide mixed line with escapes.
-		{
-			"一只 A Quick 敏捷\x1b[31m的狐 Fox 狸\x1b[0m跳过了Dog一只懒狗。",
-			"一只 A Quick 敏捷的狐 Fox 狸跳过了Dog一只懒狗。",
-			[]escapeItem{{"\x1b[31m", 13}, {"\x1b[0m", 21}},
-		},
-	}
-
-	for i, tc := range cases {
-		line2, escapes := extractTermEscapes(tc.Input)
-		if line2 != tc.Output || !reflect.DeepEqual(escapes, tc.TermEscapes) {
-			t.Fatalf("Case %d Input:\n\n`%s`\n\nExpected Output:\n\nLine: `%s`\nEscapes: `%+v`\n\nActual Output:\n\nLine: `%s`\nEscapes: `%+v`\n\n",
-				i, tc.Input, tc.Output, tc.TermEscapes, line2, escapes)
-		}
-		line3 := applyTermEscapes(line2, escapes)
-		if line3 != tc.Input {
-			t.Fatalf("Case %d Input:\n\n`%s`\n\nExpected Result:\n\n`%s`\n\nActual Result:\n\n`%s`\n\n",
-				i, tc.Input, tc.Input, line3)
-		}
-	}
-}
-
-func TestSegmentLines(t *testing.T) {
-	cases := []struct {
-		Input  string
-		Output []string
-	}{
-		// A plain ascii line with escapes.
-		{
-			"This is an example.",
-			[]string{"This", " ", "is", " ", "an", " ", "example."},
-		},
-		// A plain wide line with escapes.
-		{
-			"一只敏捷的狐狸跳过了一只懒狗。",
-			[]string{"一", "只", "敏", "捷", "的", "狐", "狸", "跳", "过",
-				"了", "一", "只", "懒", "狗", "。"},
-		},
-		// A complex stentence.
-		{
-			"This is a 'complex' example, where   一只 and English 混合了。",
-			[]string{"This", " ", "is", " ", "a", " ", "'complex'", " ", "example,",
-				" ", "where", "   ", "一", "只", " ", "and", " ", "English", " ", "混",
-				"合", "了", "。"},
-		},
-	}
-
-	for i, tc := range cases {
-		chunks := segmentLine(tc.Input)
-		if !reflect.DeepEqual(chunks, tc.Output) {
-			t.Fatalf("Case %d Input:\n\n`%s`\n\nExpected Output:\n\n`[%s]`\n\nActual Output:\n\n`[%s]`\n\n",
-				i, tc.Input, strings.Join(tc.Output, ", "), strings.Join(chunks, ", "))
-		}
-	}
-}

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)

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.

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

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
+}

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
+}

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=

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

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
+}

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)
+}

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) + "…"
+}

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)
+}