diff options
author | Yang Zhang <yang_zhang@iapcm.ac.cn> | 2018-12-26 22:49:25 +0800 |
---|---|---|
committer | Yang Zhang <yang_zhang@iapcm.ac.cn> | 2018-12-26 22:55:16 +0800 |
commit | 3fa2d15fb899c937900083fd7de696599371ce47 (patch) | |
tree | 7d8fd43e4eca7dfde65567714d3d35a6bfaf6a65 | |
parent | 8a6a8055d723e523d9943244b042c778d75b02cc (diff) | |
download | git-bug-3fa2d15fb899c937900083fd7de696599371ce47.tar.gz |
Implement displaying CJK contents
-rw-r--r-- | Gopkg.toml | 4 | ||||
-rw-r--r-- | misc/zsh_completion/git-bug | 12 | ||||
-rw-r--r-- | termui/bug_table.go | 10 | ||||
-rw-r--r-- | termui/input_popup.go | 4 | ||||
-rw-r--r-- | termui/label_select.go | 8 | ||||
-rw-r--r-- | termui/msg_popup.go | 4 | ||||
-rw-r--r-- | termui/show_bug.go | 12 | ||||
-rw-r--r-- | termui/termui.go | 4 | ||||
-rw-r--r-- | util/text/left_padded.go | 19 | ||||
-rw-r--r-- | util/text/text.go | 203 | ||||
-rw-r--r-- | util/text/text_test.go | 70 |
11 files changed, 120 insertions, 230 deletions
@@ -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) - } - } -} |