aboutsummaryrefslogtreecommitdiffstats
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
parentf5193cc76dd1a1c1bb55194a64ef90bae2278115 (diff)
parent912b5ca320891f2a4f1a88f1a137ce8ee46a1a03 (diff)
downloadgit-bug-8c7c9880b1adcf876ad63ea39e46e62bd7ebde5d.tar.gz
Merge pull request #228 from ludovicm67/patch-cli-label-colors
Display label colors in termui
-rw-r--r--.travis.yml4
-rw-r--r--Gopkg.lock16
-rw-r--r--Gopkg.toml4
-rw-r--r--bridge/github/export.go2
-rw-r--r--bug/label.go66
-rw-r--r--bug/label_test.go17
-rw-r--r--commands/bridge.go3
-rw-r--r--commands/comment.go7
-rw-r--r--commands/label_rm.go3
-rw-r--r--commands/ls.go28
-rw-r--r--commands/select.go3
-rw-r--r--git-bug.go4
-rw-r--r--graphql/resolvers/label.go2
-rw-r--r--termui/bug_table.go46
-rw-r--r--termui/label_select.go10
-rw-r--r--termui/msg_popup.go2
-rw-r--r--termui/show_bug.go11
-rw-r--r--termui/termui.go3
-rw-r--r--util/text/left_padded.go42
-rw-r--r--util/text/left_padded_test.go56
-rw-r--r--util/text/text.go330
-rw-r--r--util/text/text_test.go376
-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
-rw-r--r--vendor/github.com/theckman/goconstraint/go1.10/gte/constraint.go8
-rw-r--r--vendor/github.com/theckman/goconstraint/go1.10/gte/go110.go7
-rw-r--r--vendor/github.com/theckman/goconstraint/go1.9/gte/constraint.go8
-rw-r--r--vendor/github.com/theckman/goconstraint/go1.9/gte/go19.go7
39 files changed, 938 insertions, 896 deletions
diff --git a/.travis.yml b/.travis.yml
index 5a46a981..cac34dc7 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,13 +1,13 @@
matrix:
include:
- language: go
- go: "1.9"
- - language: go
go: "1.10"
- language: go
go: "1.11"
- language: go
go: "1.12"
+ - language: go
+ go: "1.13"
- language: node_js
node_js: 8
before_install:
diff --git a/Gopkg.lock b/Gopkg.lock
index 1e499fe9..194e6c33 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -25,6 +25,14 @@
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"
name = "github.com/MichaelMure/gocui"
@@ -313,9 +321,9 @@
version = "v1.2.2"
[[projects]]
- digest = "1:823766f4e1833bd562339317d905475fe46789da3863b2da7a1871f9f12bb4b3"
+ digest = "1:8d784da270f610f505e2c9785faec4c81d4a6a6a1034412685a4bf2cffe24cef"
name = "github.com/theckman/goconstraint"
- packages = ["go1.9/gte"]
+ packages = ["go1.10/gte"]
pruneopts = "UT"
revision = "93babf24513d0e8277635da8169fcc5a46ae3f6a"
version = "v1.11.0"
@@ -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",
@@ -482,7 +490,7 @@
"github.com/spf13/cobra/doc",
"github.com/stretchr/testify/assert",
"github.com/stretchr/testify/require",
- "github.com/theckman/goconstraint/go1.9/gte",
+ "github.com/theckman/goconstraint/go1.10/gte",
"github.com/vektah/gqlgen/client",
"github.com/vektah/gqlparser",
"github.com/vektah/gqlparser/ast",
diff --git a/Gopkg.toml b/Gopkg.toml
index 72eebde5..3ec5b80e 100644
--- a/Gopkg.toml
+++ b/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"
diff --git a/bridge/github/export.go b/bridge/github/export.go
index b239eff9..a79256fc 100644
--- a/bridge/github/export.go
+++ b/bridge/github/export.go
@@ -576,7 +576,7 @@ func (ge *githubExporter) getOrCreateGithubLabelID(ctx context.Context, gc *gith
}
// RGBA to hex color
- rgba := label.RGBA()
+ rgba := label.Color().RGBA()
hexColor := fmt.Sprintf("%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B)
ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
diff --git a/bug/label.go b/bug/label.go
index 0d6d4142..1344d97e 100644
--- a/bug/label.go
+++ b/bug/label.go
@@ -15,32 +15,34 @@ func (l Label) String() string {
return string(l)
}
+type LabelColor color.RGBA
+
// RGBA from a Label computed in a deterministic way
-func (l Label) RGBA() color.RGBA {
+func (l Label) Color() LabelColor {
id := 0
hash := sha1.Sum([]byte(l))
// colors from: https://material-ui.com/style/color/
- colors := []color.RGBA{
- color.RGBA{R: 244, G: 67, B: 54, A: 255}, // red
- color.RGBA{R: 233, G: 30, B: 99, A: 255}, // pink
- color.RGBA{R: 156, G: 39, B: 176, A: 255}, // purple
- color.RGBA{R: 103, G: 58, B: 183, A: 255}, // deepPurple
- color.RGBA{R: 63, G: 81, B: 181, A: 255}, // indigo
- color.RGBA{R: 33, G: 150, B: 243, A: 255}, // blue
- color.RGBA{R: 3, G: 169, B: 244, A: 255}, // lightBlue
- color.RGBA{R: 0, G: 188, B: 212, A: 255}, // cyan
- color.RGBA{R: 0, G: 150, B: 136, A: 255}, // teal
- color.RGBA{R: 76, G: 175, B: 80, A: 255}, // green
- color.RGBA{R: 139, G: 195, B: 74, A: 255}, // lightGreen
- color.RGBA{R: 205, G: 220, B: 57, A: 255}, // lime
- color.RGBA{R: 255, G: 235, B: 59, A: 255}, // yellow
- color.RGBA{R: 255, G: 193, B: 7, A: 255}, // amber
- color.RGBA{R: 255, G: 152, B: 0, A: 255}, // orange
- color.RGBA{R: 255, G: 87, B: 34, A: 255}, // deepOrange
- color.RGBA{R: 121, G: 85, B: 72, A: 255}, // brown
- color.RGBA{R: 158, G: 158, B: 158, A: 255}, // grey
- color.RGBA{R: 96, G: 125, B: 139, A: 255}, // blueGrey
+ colors := []LabelColor{
+ LabelColor{R: 244, G: 67, B: 54, A: 255}, // red
+ LabelColor{R: 233, G: 30, B: 99, A: 255}, // pink
+ LabelColor{R: 156, G: 39, B: 176, A: 255}, // purple
+ LabelColor{R: 103, G: 58, B: 183, A: 255}, // deepPurple
+ LabelColor{R: 63, G: 81, B: 181, A: 255}, // indigo
+ LabelColor{R: 33, G: 150, B: 243, A: 255}, // blue
+ LabelColor{R: 3, G: 169, B: 244, A: 255}, // lightBlue
+ LabelColor{R: 0, G: 188, B: 212, A: 255}, // cyan
+ LabelColor{R: 0, G: 150, B: 136, A: 255}, // teal
+ LabelColor{R: 76, G: 175, B: 80, A: 255}, // green
+ LabelColor{R: 139, G: 195, B: 74, A: 255}, // lightGreen
+ LabelColor{R: 205, G: 220, B: 57, A: 255}, // lime
+ LabelColor{R: 255, G: 235, B: 59, A: 255}, // yellow
+ LabelColor{R: 255, G: 193, B: 7, A: 255}, // amber
+ LabelColor{R: 255, G: 152, B: 0, A: 255}, // orange
+ LabelColor{R: 255, G: 87, B: 34, A: 255}, // deepOrange
+ LabelColor{R: 121, G: 85, B: 72, A: 255}, // brown
+ LabelColor{R: 158, G: 158, B: 158, A: 255}, // grey
+ LabelColor{R: 96, G: 125, B: 139, A: 255}, // blueGrey
}
for _, char := range hash {
@@ -50,6 +52,28 @@ func (l Label) RGBA() color.RGBA {
return colors[id]
}
+func (lc LabelColor) RGBA() color.RGBA {
+ return color.RGBA(lc)
+}
+
+type Term256 int
+
+func (lc LabelColor) Term256() Term256 {
+ red := Term256(lc.R) * 6 / 256
+ green := Term256(lc.G) * 6 / 256
+ blue := Term256(lc.B) * 6 / 256
+
+ return red*36 + green*6 + blue + 16
+}
+
+func (t Term256) Escape() string {
+ return fmt.Sprintf("\x1b[38;5;%dm", t)
+}
+
+func (t Term256) Unescape() string {
+ return "\x1b[0m"
+}
+
func (l Label) Validate() error {
str := string(l)
diff --git a/bug/label_test.go b/bug/label_test.go
index f87c7411..225e1352 100644
--- a/bug/label_test.go
+++ b/bug/label_test.go
@@ -1,36 +1,35 @@
package bug
import (
- "image/color"
"testing"
"github.com/stretchr/testify/require"
)
func TestLabelRGBA(t *testing.T) {
- rgba := Label("test").RGBA()
- expected := color.RGBA{R: 255, G: 87, B: 34, A: 255}
+ rgba := Label("test").Color()
+ expected := LabelColor{R: 255, G: 87, B: 34, A: 255}
require.Equal(t, expected, rgba)
}
func TestLabelRGBASimilar(t *testing.T) {
- rgba := Label("test1").RGBA()
- expected := color.RGBA{R: 0, G: 188, B: 212, A: 255}
+ rgba := Label("test1").Color()
+ expected := LabelColor{R: 0, G: 188, B: 212, A: 255}
require.Equal(t, expected, rgba)
}
func TestLabelRGBAReverse(t *testing.T) {
- rgba := Label("tset").RGBA()
- expected := color.RGBA{R: 233, G: 30, B: 99, A: 255}
+ rgba := Label("tset").Color()
+ expected := LabelColor{R: 233, G: 30, B: 99, A: 255}
require.Equal(t, expected, rgba)
}
func TestLabelRGBAEqual(t *testing.T) {
- color1 := Label("test").RGBA()
- color2 := Label("test").RGBA()
+ color1 := Label("test").Color()
+ color2 := Label("test").Color()
require.Equal(t, color1, color2)
}
diff --git a/commands/bridge.go b/commands/bridge.go
index 2566fd06..3c398e6b 100644
--- a/commands/bridge.go
+++ b/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 {
diff --git a/commands/comment.go b/commands/comment.go
index 33bae65d..4be39a84 100644
--- a/commands/comment.go
+++ b/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))
}
}
diff --git a/commands/label_rm.go b/commands/label_rm.go
index a0c1c56d..11300c78 100644
--- a/commands/label_rm.go
+++ b/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 {
diff --git a/commands/ls.go b/commands/ls.go
index 9c32642e..70a948e6 100644
--- a/commands/ls.go
+++ b/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,17 +66,30 @@ func runLsBug(cmd *cobra.Command, args []string) error {
name = b.LegacyAuthor.DisplayName()
}
+ var labelsTxt strings.Builder
+ for _, l := range b.Labels {
+ lc256 := l.Color().Term256()
+ labelsTxt.WriteString(lc256.Escape())
+ labelsTxt.WriteString(" ◼")
+ labelsTxt.WriteString(lc256.Unescape())
+ }
+
// truncate + pad if needed
- titleFmt := text.LeftPadMaxLine(b.Title, 50, 0)
+ labelsFmt := text.TruncateMax(labelsTxt.String(), 10)
+ titleFmt := text.LeftPadMaxLine(b.Title, 50-text.Len(labelsFmt), 0)
authorFmt := text.LeftPadMaxLine(name, 15, 0)
- fmt.Printf("%s %s\t%s\t%s\tC:%d L:%d\n",
+ comments := fmt.Sprintf("%4d 💬", b.LenComments)
+ if b.LenComments > 9999 {
+ comments = " ∞ 💬"
+ }
+
+ fmt.Printf("%s %s\t%s\t%s\t%s\n",
colors.Cyan(b.Id.Human()),
colors.Yellow(b.Status),
- titleFmt,
+ titleFmt+labelsFmt,
colors.Magenta(authorFmt),
- b.LenComments,
- len(b.Labels),
+ comments,
)
}
diff --git a/commands/select.go b/commands/select.go
index 7c40df5c..f2ae33ca 100644
--- a/commands/select.go
+++ b/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 {
diff --git a/git-bug.go b/git-bug.go
index cf59182f..50415ae2 100644
--- a/git-bug.go
+++ b/git-bug.go
@@ -9,8 +9,8 @@ package main
import (
"github.com/MichaelMure/git-bug/commands"
- // minimal go version is 1.9
- _ "github.com/theckman/goconstraint/go1.9/gte"
+ // minimal go version is 1.10
+ _ "github.com/theckman/goconstraint/go1.10/gte"
)
func main() {
diff --git a/graphql/resolvers/label.go b/graphql/resolvers/label.go
index 690bf7f6..0368a1e6 100644
--- a/graphql/resolvers/label.go
+++ b/graphql/resolvers/label.go
@@ -19,7 +19,7 @@ func (labelResolver) Name(ctx context.Context, obj *bug.Label) (string, error) {
}
func (labelResolver) Color(ctx context.Context, obj *bug.Label) (*color.RGBA, error) {
- rgba := obj.RGBA()
+ rgba := obj.Color().RGBA()
return &rgba, nil
}
diff --git a/termui/bug_table.go b/termui/bug_table.go
index 8d69d665..c432c94a 100644
--- a/termui/bug_table.go
+++ b/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"
@@ -275,8 +277,8 @@ func (bt *bugTable) getColumnWidths(maxX int) map[string]int {
left := maxX - 5 - m["id"] - m["status"]
- m["summary"] = 10
- left -= m["summary"]
+ m["comments"] = 10
+ left -= m["comments"]
m["lastEdit"] = 19
left -= m["lastEdit"]
@@ -290,10 +292,21 @@ func (bt *bugTable) render(v *gocui.View, maxX int) {
columnWidths := bt.getColumnWidths(maxX)
for _, excerpt := range bt.excerpts {
- summaryTxt := fmt.Sprintf("C:%-2d L:%-2d",
- excerpt.LenComments,
- len(excerpt.Labels),
- )
+ summaryTxt := fmt.Sprintf("%4d 💬", excerpt.LenComments)
+ if excerpt.LenComments <= 0 {
+ summaryTxt = ""
+ }
+ if excerpt.LenComments > 9999 {
+ summaryTxt = " ∞ 💬"
+ }
+
+ var labelsTxt strings.Builder
+ for _, l := range excerpt.Labels {
+ lc256 := l.Color().Term256()
+ labelsTxt.WriteString(lc256.Escape())
+ labelsTxt.WriteString(" ◼")
+ labelsTxt.WriteString(lc256.Unescape())
+ }
var authorDisplayName string
if excerpt.AuthorId != "" {
@@ -310,17 +323,19 @@ 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"], 1)
+ 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)
- summary := text.LeftPadMaxLine(summaryTxt, columnWidths["summary"], 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),
- summary,
+ comments,
lastEdit,
)
}
@@ -333,12 +348,11 @@ func (bt *bugTable) renderHeader(v *gocui.View, maxX int) {
status := text.LeftPadMaxLine("STATUS", columnWidths["status"], 1)
title := text.LeftPadMaxLine("TITLE", columnWidths["title"], 1)
author := text.LeftPadMaxLine("AUTHOR", columnWidths["author"], 1)
- summary := text.LeftPadMaxLine("SUMMARY", columnWidths["summary"], 1)
+ comments := text.LeftPadMaxLine("COMMENTS", columnWidths["comments"], 1)
lastEdit := text.LeftPadMaxLine("LAST EDIT", columnWidths["lastEdit"], 1)
_, _ = fmt.Fprintf(v, "\n")
- _, _ = fmt.Fprintf(v, "%s %s %s %s %s %s\n", id, status, title, author, summary, lastEdit)
-
+ _, _ = fmt.Fprintf(v, "%s %s %s %s %s %s\n", id, status, title, author, comments, lastEdit)
}
func (bt *bugTable) renderFooter(v *gocui.View, maxX int) {
diff --git a/termui/label_select.go b/termui/label_select.go
index 131703f9..39edbdb1 100644
--- a/termui/label_select.go
+++ b/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"
@@ -127,7 +128,12 @@ func (ls *labelSelect) layout(g *gocui.Gui) error {
if ls.labelSelect[i] {
selectBox = " [x] "
}
- fmt.Fprint(v, selectBox, label)
+
+ lc := label.Color()
+ lc256 := lc.Term256()
+ labelStr := lc256.Escape() + "◼ " + lc256.Unescape() + label.String()
+ fmt.Fprint(v, selectBox, labelStr)
+
y0 += 2
}
diff --git a/termui/msg_popup.go b/termui/msg_popup.go
index 4452427e..99180c99 100644
--- a/termui/msg_popup.go
+++ b/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"
)
diff --git a/termui/show_bug.go b/termui/show_bug.go
index 228b85b0..50478b8f 100644
--- a/termui/show_bug.go
+++ b/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"
@@ -429,13 +430,15 @@ func (sb *showBug) renderSidebar(g *gocui.Gui, sideView *gocui.View) error {
labelStr := make([]string, len(snap.Labels))
for i, l := range snap.Labels {
- labelStr[i] = string(l)
+ lc := l.Color()
+ lc256 := lc.Term256()
+ labelStr[i] = lc256.Escape() + "◼ " + lc256.Unescape() + l.String()
}
labels := strings.Join(labelStr, "\n")
labels, lines := text.WrapLeftPadded(labels, maxX, 2)
- content := fmt.Sprintf("%s\n\n%s", colors.Bold("Labels"), labels)
+ content := fmt.Sprintf("%s\n\n%s", colors.Bold(" Labels"), labels)
v, err := sb.createSideView(g, "sideLabels", x0, y0, maxX, lines+2)
if err != nil {
diff --git a/termui/termui.go b/termui/termui.go
index 5d3bb0c1..8aece020 100644
--- a/termui/termui.go
+++ b/termui/termui.go
@@ -66,11 +66,12 @@ func Run(cache *cache.RepoCache) error {
return err
}
+
return nil
}
func initGui(action func(ui *termUI) error) {
- g, err := gocui.NewGui(gocui.OutputNormal)
+ g, err := gocui.NewGui(gocui.Output256)
if err != nil {
ui.gError <- err
diff --git a/util/text/left_padded.go b/util/text/left_padded.go
deleted file mode 100644
index eae65d34..00000000
--- a/util/text/left_padded.go
+++ /dev/null
@@ -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()
-}
diff --git a/util/text/left_padded_test.go b/util/text/left_padded_test.go
deleted file mode 100644
index 0be79e32..00000000
--- a/util/text/left_padded_test.go
+++ /dev/null
@@ -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)
- }
- }
-}
diff --git a/util/text/text.go b/util/text/text.go
deleted file mode 100644
index 39584d5d..00000000
--- a/util/text/text.go
+++ /dev/null
@@ -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)
-}
diff --git a/util/text/text_test.go b/util/text/text_test.go
deleted file mode 100644
index 5be25409..00000000
--- a/util/text/text_test.go
+++ /dev/null
@@ -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, ", "))
- }
- }
-}
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)
+}
diff --git a/vendor/github.com/theckman/goconstraint/go1.10/gte/constraint.go b/vendor/github.com/theckman/goconstraint/go1.10/gte/constraint.go
new file mode 100644
index 00000000..33dc1161
--- /dev/null
+++ b/vendor/github.com/theckman/goconstraint/go1.10/gte/constraint.go
@@ -0,0 +1,8 @@
+// The contents of this file has been released in to the Public Domain.
+
+// Package gtego110 should only be used as a blank import. If imported, it
+// will only compile if the Go runtime version is >= 1.10.
+package gtego110
+
+// This will fail to compile if the Go runtime version isn't >= 1.10.
+var _ = __SOFTWARE_REQUIRES_GO_VERSION_1_10__
diff --git a/vendor/github.com/theckman/goconstraint/go1.10/gte/go110.go b/vendor/github.com/theckman/goconstraint/go1.10/gte/go110.go
new file mode 100644
index 00000000..5d586ff9
--- /dev/null
+++ b/vendor/github.com/theckman/goconstraint/go1.10/gte/go110.go
@@ -0,0 +1,7 @@
+// The contents of this file has been released in to the Public Domain.
+
+// +build go1.10
+
+package gtego110
+
+const __SOFTWARE_REQUIRES_GO_VERSION_1_10__ = uint8(0)
diff --git a/vendor/github.com/theckman/goconstraint/go1.9/gte/constraint.go b/vendor/github.com/theckman/goconstraint/go1.9/gte/constraint.go
deleted file mode 100644
index ed67fefc..00000000
--- a/vendor/github.com/theckman/goconstraint/go1.9/gte/constraint.go
+++ /dev/null
@@ -1,8 +0,0 @@
-// The contents of this file has been released in to the Public Domain.
-
-// Package gtego19 should only be used as a blank import. If imported, it
-// will only compile if the Go runtime version is >= 1.9.
-package gtego19
-
-// This will fail to compile if the Go runtime version isn't >= 1.9.
-var _ = __SOFTWARE_REQUIRES_GO_VERSION_1_9__
diff --git a/vendor/github.com/theckman/goconstraint/go1.9/gte/go19.go b/vendor/github.com/theckman/goconstraint/go1.9/gte/go19.go
deleted file mode 100644
index 446a5ee3..00000000
--- a/vendor/github.com/theckman/goconstraint/go1.9/gte/go19.go
+++ /dev/null
@@ -1,7 +0,0 @@
-// The contents of this file has been released in to the Public Domain.
-
-// +build go1.9
-
-package gtego19
-
-const __SOFTWARE_REQUIRES_GO_VERSION_1_9__ = uint8(0)