aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Gopkg.toml4
-rw-r--r--misc/zsh_completion/git-bug12
-rw-r--r--termui/bug_table.go10
-rw-r--r--termui/input_popup.go4
-rw-r--r--termui/label_select.go8
-rw-r--r--termui/msg_popup.go4
-rw-r--r--termui/show_bug.go12
-rw-r--r--termui/termui.go4
-rw-r--r--util/text/left_padded.go19
-rw-r--r--util/text/text.go203
-rw-r--r--util/text/text_test.go70
11 files changed, 120 insertions, 230 deletions
diff --git a/Gopkg.toml b/Gopkg.toml
index 79acb49d..93a89ad6 100644
--- a/Gopkg.toml
+++ b/Gopkg.toml
@@ -61,5 +61,5 @@
version = "0.7.1"
[[constraint]]
- name = "github.com/jroimartin/gocui"
- branch = "master" \ No newline at end of file
+ name = "github.com/jesseduffield/gocui"
+ branch = "master"
diff --git a/misc/zsh_completion/git-bug b/misc/zsh_completion/git-bug
index 2deae548..14ce5ba9 100644
--- a/misc/zsh_completion/git-bug
+++ b/misc/zsh_completion/git-bug
@@ -17,6 +17,12 @@ case $state in
;;
level2)
case $words[2] in
+ status)
+ _arguments '2: :(close open)'
+ ;;
+ title)
+ _arguments '2: :(edit)'
+ ;;
bridge)
_arguments '2: :(configure pull rm)'
;;
@@ -26,12 +32,6 @@ case $state in
label)
_arguments '2: :(add rm)'
;;
- status)
- _arguments '2: :(close open)'
- ;;
- title)
- _arguments '2: :(edit)'
- ;;
*)
_arguments '*: :_files'
;;
diff --git a/termui/bug_table.go b/termui/bug_table.go
index 1545dbc9..fb281d74 100644
--- a/termui/bug_table.go
+++ b/termui/bug_table.go
@@ -9,7 +9,7 @@ import (
"github.com/MichaelMure/git-bug/util/colors"
"github.com/MichaelMure/git-bug/util/text"
"github.com/dustin/go-humanize"
- "github.com/jroimartin/gocui"
+ "github.com/jesseduffield/gocui"
)
const bugTableView = "bugTableView"
@@ -53,7 +53,7 @@ func (bt *bugTable) layout(g *gocui.Gui) error {
return nil
}
- v, err := g.SetView(bugTableHeaderView, -1, -1, maxX, 3)
+ v, err := g.SetView(bugTableHeaderView, -1, -1, maxX, 3, 0)
if err != nil {
if err != gocui.ErrUnknownView {
@@ -66,7 +66,7 @@ func (bt *bugTable) layout(g *gocui.Gui) error {
v.Clear()
bt.renderHeader(v, maxX)
- v, err = g.SetView(bugTableView, -1, 1, maxX, maxY-3)
+ v, err = g.SetView(bugTableView, -1, 1, maxX, maxY-3, 0)
if err != nil {
if err != gocui.ErrUnknownView {
@@ -97,7 +97,7 @@ func (bt *bugTable) layout(g *gocui.Gui) error {
v.Clear()
bt.render(v, maxX)
- v, err = g.SetView(bugTableFooterView, -1, maxY-4, maxX, maxY)
+ v, err = g.SetView(bugTableFooterView, -1, maxY-4, maxX, maxY, 0)
if err != nil {
if err != gocui.ErrUnknownView {
@@ -110,7 +110,7 @@ func (bt *bugTable) layout(g *gocui.Gui) error {
v.Clear()
bt.renderFooter(v, maxX)
- v, err = g.SetView(bugTableInstructionView, -1, maxY-2, maxX, maxY)
+ v, err = g.SetView(bugTableInstructionView, -1, maxY-2, maxX, maxY, 0)
if err != nil {
if err != gocui.ErrUnknownView {
diff --git a/termui/input_popup.go b/termui/input_popup.go
index db0ec619..7500168f 100644
--- a/termui/input_popup.go
+++ b/termui/input_popup.go
@@ -3,7 +3,7 @@ package termui
import (
"io/ioutil"
- "github.com/jroimartin/gocui"
+ "github.com/jesseduffield/gocui"
)
const inputPopupView = "inputPopupView"
@@ -46,7 +46,7 @@ func (ip *inputPopup) layout(g *gocui.Gui) error {
x0 := (maxX - width) / 2
y0 := (maxY - height) / 2
- v, err := g.SetView(inputPopupView, x0, y0, x0+width, y0+height)
+ v, err := g.SetView(inputPopupView, x0, y0, x0+width, y0+height, 0)
if err != nil {
if err != gocui.ErrUnknownView {
return err
diff --git a/termui/label_select.go b/termui/label_select.go
index 244001df..c93dd833 100644
--- a/termui/label_select.go
+++ b/termui/label_select.go
@@ -6,7 +6,7 @@ import (
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache"
- "github.com/jroimartin/gocui"
+ "github.com/jesseduffield/gocui"
)
const labelSelectView = "labelSelectView"
@@ -105,7 +105,7 @@ func (ls *labelSelect) layout(g *gocui.Gui) error {
x0 := 1
y0 := 0 - ls.scroll
- v, err := g.SetView(labelSelectView, x0, 0, x0+width, maxY-2)
+ v, err := g.SetView(labelSelectView, x0, 0, x0+width, maxY-2, 0)
if err != nil {
if err != gocui.ErrUnknownView {
return err
@@ -116,7 +116,7 @@ func (ls *labelSelect) layout(g *gocui.Gui) error {
for i, label := range ls.labels {
viewname := fmt.Sprintf("view%d", i)
- v, err := g.SetView(viewname, x0+2, y0, x0+width-2, y0+2)
+ v, err := g.SetView(viewname, x0+2, y0, x0+width-2, y0+2, 0)
if err != nil && err != gocui.ErrUnknownView {
return err
}
@@ -131,7 +131,7 @@ func (ls *labelSelect) layout(g *gocui.Gui) error {
y0 += 2
}
- v, err = g.SetView(labelSelectInstructionsView, -1, maxY-2, maxX, maxY)
+ v, err = g.SetView(labelSelectInstructionsView, -1, maxY-2, maxX, maxY, 0)
ls.childViews = append(ls.childViews, labelSelectInstructionsView)
if err != nil {
if err != gocui.ErrUnknownView {
diff --git a/termui/msg_popup.go b/termui/msg_popup.go
index 0ce390dc..af752106 100644
--- a/termui/msg_popup.go
+++ b/termui/msg_popup.go
@@ -4,7 +4,7 @@ import (
"fmt"
"github.com/MichaelMure/git-bug/util/text"
- "github.com/jroimartin/gocui"
+ "github.com/jesseduffield/gocui"
)
const msgPopupView = "msgPopupView"
@@ -50,7 +50,7 @@ func (ep *msgPopup) layout(g *gocui.Gui) error {
x0 := (maxX - width) / 2
y0 := (maxY - height) / 2
- v, err := g.SetView(msgPopupView, x0, y0, x0+width, y0+height)
+ v, err := g.SetView(msgPopupView, x0, y0, x0+width, y0+height, 0)
if err != nil {
if err != gocui.ErrUnknownView {
return err
diff --git a/termui/show_bug.go b/termui/show_bug.go
index a1f4b3fe..49754d83 100644
--- a/termui/show_bug.go
+++ b/termui/show_bug.go
@@ -10,7 +10,7 @@ import (
"github.com/MichaelMure/git-bug/util/colors"
"github.com/MichaelMure/git-bug/util/git"
"github.com/MichaelMure/git-bug/util/text"
- "github.com/jroimartin/gocui"
+ "github.com/jesseduffield/gocui"
)
const showBugView = "showBugView"
@@ -48,7 +48,7 @@ func (sb *showBug) layout(g *gocui.Gui) error {
maxX, maxY := g.Size()
sb.childViews = nil
- v, err := g.SetView(showBugView, 0, 0, maxX*2/3, maxY-2)
+ v, err := g.SetView(showBugView, 0, 0, maxX*2/3, maxY-2, 0)
if err != nil {
if err != gocui.ErrUnknownView {
@@ -65,7 +65,7 @@ func (sb *showBug) layout(g *gocui.Gui) error {
return err
}
- v, err = g.SetView(showBugSidebarView, maxX*2/3+1, 0, maxX-1, maxY-2)
+ v, err = g.SetView(showBugSidebarView, maxX*2/3+1, 0, maxX-1, maxY-2, 0)
if err != nil {
if err != gocui.ErrUnknownView {
@@ -82,7 +82,7 @@ func (sb *showBug) layout(g *gocui.Gui) error {
return err
}
- v, err = g.SetView(showBugInstructionView, -1, maxY-2, maxX, maxY)
+ v, err = g.SetView(showBugInstructionView, -1, maxY-2, maxX, maxY, 0)
if err != nil {
if err != gocui.ErrUnknownView {
@@ -382,7 +382,7 @@ func emptyMessagePlaceholder() string {
}
func (sb *showBug) createOpView(g *gocui.Gui, name string, x0 int, y0 int, maxX int, height int, selectable bool) (*gocui.View, error) {
- v, err := g.SetView(name, x0, y0, maxX, y0+height+1)
+ v, err := g.SetView(name, x0, y0, maxX, y0+height+1, 0)
if err != nil && err != gocui.ErrUnknownView {
return nil, err
@@ -402,7 +402,7 @@ func (sb *showBug) createOpView(g *gocui.Gui, name string, x0 int, y0 int, maxX
}
func (sb *showBug) createSideView(g *gocui.Gui, name string, x0 int, y0 int, maxX int, height int) (*gocui.View, error) {
- v, err := g.SetView(name, x0, y0, maxX, y0+height+1)
+ v, err := g.SetView(name, x0, y0, maxX, y0+height+1, 0)
if err != nil && err != gocui.ErrUnknownView {
return nil, err
diff --git a/termui/termui.go b/termui/termui.go
index 9f68efcc..d11a57ba 100644
--- a/termui/termui.go
+++ b/termui/termui.go
@@ -5,7 +5,7 @@ import (
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/input"
"github.com/MichaelMure/git-bug/util/git"
- "github.com/jroimartin/gocui"
+ "github.com/jesseduffield/gocui"
"github.com/pkg/errors"
)
@@ -69,7 +69,7 @@ func Run(cache *cache.RepoCache) error {
}
func initGui(action func(ui *termUI) error) {
- g, err := gocui.NewGui(gocui.OutputNormal)
+ g, err := gocui.NewGui(gocui.OutputNormal, false)
if err != nil {
ui.gError <- err
diff --git a/util/text/left_padded.go b/util/text/left_padded.go
index 729834db..3b8e13c6 100644
--- a/util/text/left_padded.go
+++ b/util/text/left_padded.go
@@ -3,25 +3,26 @@ 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
+// 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 {
- runes := []rune(text)
+ rightPart := text
+ scrWidth := runewidth.StringWidth(text)
// truncate and ellipse if needed
- if len(runes)+leftPad > length {
- runes = append(runes[:(length-leftPad-1)], '…')
- }
-
- if len(runes)+leftPad < length {
- runes = append(runes, []rune(strings.Repeat(" ", length-len(runes)-leftPad))...)
+ 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),
- string(runes),
+ rightPart,
)
}
diff --git a/util/text/text.go b/util/text/text.go
index c000596c..f8910c2e 100644
--- a/util/text/text.go
+++ b/util/text/text.go
@@ -2,6 +2,7 @@ package text
import (
"bytes"
+ "github.com/mattn/go-runewidth"
"strings"
)
@@ -15,110 +16,110 @@ func Wrap(text string, lineWidth int) (string, int) {
// Handle properly terminal color escape code
func WrapLeftPadded(text string, lineWidth int, leftPad int) (string, int) {
var textBuffer bytes.Buffer
- var lineBuffer bytes.Buffer
- nbLine := 1
- firstLine := true
+ nbLine := 0
pad := strings.Repeat(" ", leftPad)
// tabs are formatted as 4 spaces
text = strings.Replace(text, "\t", " ", 4)
+ wrapped := wrapText(text, lineWidth-leftPad)
+ for _, line := range strings.Split(wrapped, "\n") {
+ textBuffer.WriteString(pad + line)
+ textBuffer.WriteString("\n")
+ nbLine++
+ }
+ return textBuffer.String(), nbLine
+}
- for _, line := range strings.Split(text, "\n") {
- spaceLeft := lineWidth - leftPad
-
- if !firstLine {
- textBuffer.WriteString("\n")
- nbLine++
- }
-
- firstWord := true
-
- for _, word := range strings.Split(line, " ") {
- wordLength := wordLen(word)
-
- if !firstWord {
- lineBuffer.WriteString(" ")
- spaceLeft -= 1
-
- if spaceLeft <= 0 {
- textBuffer.WriteString(pad + strings.TrimRight(lineBuffer.String(), " "))
- textBuffer.WriteString("\n")
- lineBuffer.Reset()
- spaceLeft = lineWidth - leftPad
- nbLine++
- firstLine = false
- }
+// Wrap text so that each line fills at most w cells. Lines break at word
+// boundary or multibyte chars.
+//
+// Wrapping Algorithm: Treat the text as a sequence of words, with each word be
+// an alphanumeric word, or a multibyte char. We scan through the text and
+// construct the word, and flush the word into the paragraph once a word is
+// ready. A word is ready when a word boundary is detected: a boundary char such
+// as '\n', '\t', and ' ' is encountered; a multibyte char is found; or a
+// multibyte to single-byte switch is encountered. '\n' is handled in a special
+// manner.
+func wrapText(s string, w int) string {
+ word := ""
+ out := ""
+
+ width := 0
+ firstWord := true
+ isMultibyteWord := false
+
+ flushWord := func() {
+ wl := wordLen(word)
+ if isMultibyteWord {
+ if width+wl > w {
+ out += "\n" + word
+ width = wl
+ } else {
+ out += word
+ width += wl
}
-
- // Word fit in the current line
- if spaceLeft >= wordLength {
- lineBuffer.WriteString(word)
- spaceLeft -= wordLength
- firstWord = false
+ } else {
+ if width == 0 {
+ out += word
+ width += wl
+ } else if width+wl+1 > w {
+ out += "\n" + word
+ width = wl
} else {
- // Break a word longer than a line
- if wordLength > lineWidth {
- for wordLength > 0 && wordLen(word) > 0 {
- l := minInt(spaceLeft, wordLength)
- part, leftover := splitWord(word, l)
- word = leftover
- wordLength = wordLen(word)
-
- lineBuffer.WriteString(part)
- textBuffer.WriteString(pad)
- textBuffer.Write(lineBuffer.Bytes())
- lineBuffer.Reset()
-
- spaceLeft -= l
-
- if spaceLeft <= 0 {
- textBuffer.WriteString("\n")
- nbLine++
- spaceLeft = lineWidth - leftPad
- }
-
- if wordLength <= 0 {
- break
- }
- }
- } else {
- // Normal break
- textBuffer.WriteString(pad + strings.TrimRight(lineBuffer.String(), " "))
- textBuffer.WriteString("\n")
- lineBuffer.Reset()
- lineBuffer.WriteString(word)
- firstWord = false
- spaceLeft = lineWidth - leftPad - wordLength
- nbLine++
- }
+ out += " " + word
+ width += wl + 1
}
}
+ word = ""
+ }
- if lineBuffer.Len() > 0 {
- textBuffer.WriteString(pad + strings.TrimRight(lineBuffer.String(), " "))
- lineBuffer.Reset()
+ for _, r := range []rune(s) {
+ cw := runewidth.RuneWidth(r)
+ if firstWord {
+ word = string(r)
+ isMultibyteWord = cw > 1
+ firstWord = false
+ continue
+ }
+ if r == '\n' {
+ flushWord()
+ out += "\n"
+ width = 0
+ } else if r == ' ' || r == '\t' {
+ flushWord()
+ } else if cw > 1 {
+ flushWord()
+ word = string(r)
+ isMultibyteWord = true
+ word = string(r)
+ } else if cw == 1 && isMultibyteWord {
+ flushWord()
+ word = string(r)
+ isMultibyteWord = false
+ } else {
+ word += string(r)
}
-
- firstLine = false
}
+ // The text may end without newlines, ensure flushing it or we can lose the
+ // last word.
+ flushWord()
- return textBuffer.String(), nbLine
+ return out
}
-// wordLen return the length of a word, while ignoring the terminal escape sequences
+// 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 {
+ for _, char := range []rune(word) {
if char == '\x1b' {
escape = true
}
-
if !escape {
- length++
+ length += runewidth.RuneWidth(char)
}
-
if char == 'm' {
escape = false
}
@@ -126,45 +127,3 @@ func wordLen(word string) int {
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
- }
-
- result = append(result, r)
-
- if !escape {
- added++
- if added == length {
- break
- }
- }
-
- if r == 'm' {
- escape = false
- }
- }
-
- leftover := runes[len(result):]
-
- return string(result), string(leftover)
-}
-
-func minInt(a, b int) int {
- if a > b {
- return b
- }
- return a
-}
diff --git a/util/text/text_test.go b/util/text/text_test.go
index cf72431e..38e747df 100644
--- a/util/text/text_test.go
+++ b/util/text/text_test.go
@@ -199,73 +199,3 @@ func TestWordLen(t *testing.T) {
}
}
}
-
-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
- {
- "快檢什麼望對",
- 2,
- "快檢", "什麼望對",
- },
- {
- "快檢什麼望對",
- 3,
- "快檢什", "麼望對",
- },
- // Handle chinese with colors
- {
- "快\x1b[31m檢什麼\x1b[0m望對",
- 2,
- "快\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)
- }
- }
-}