From b436024302b247a464dac773ce0642b3ca69e522 Mon Sep 17 00:00:00 2001 From: gizak Date: Wed, 25 Mar 2015 18:04:15 -0400 Subject: [PATCH] Fix https://github.com/gizak/termui/issues/10 Handle CJK wide characters in helper.go Add multi width text ouput support in widgets --- bar.go | 6 ++++-- box.go | 6 ++++-- chart.go | 17 ++++++++++------- example/list.go | 4 ++-- example/par.go | 4 ++-- helper.go | 16 ++++++++++++---- helper_test.go | 45 ++++++++++++++++++++++++++++++++++++++++++++- list.go | 6 ++++-- p.go | 9 ++++++--- sparkline.go | 7 +++++-- 10 files changed, 93 insertions(+), 27 deletions(-) diff --git a/bar.go b/bar.go index d0b4ec0..025ce26 100644 --- a/bar.go +++ b/bar.go @@ -91,14 +91,16 @@ func (bc *BarChart) Buffer() []Point { } } // plot text - for j := 0; j < len(bc.labels[i]); j++ { + for j, k := 0, 0; j < len(bc.labels[i]); j++ { + w := charWidth(bc.labels[i][j]) p := Point{} p.Ch = bc.labels[i][j] p.Bg = bc.BgColor p.Fg = bc.TextColor p.Y = bc.innerY + bc.innerHeight - 1 - p.X = bc.innerX + oftX + j + p.X = bc.innerX + oftX + k ps = append(ps, p) + k += w } // plot num for j := 0; j < len(bc.dataNum[i]); j++ { diff --git a/box.go b/box.go index 6b500ed..1dcfd86 100644 --- a/box.go +++ b/box.go @@ -107,8 +107,10 @@ func (lb labeledBorder) Buffer() []Point { maxTxtW := lb.Width - 2 rs := trimStr2Runes(lb.Label, maxTxtW) - for i := 0; i < len(rs); i++ { - ps = append(ps, newPointWithAttrs(rs[i], lb.X+1+i, lb.Y, lb.LabelFgColor, lb.LabelBgColor)) + for i, j, w := 0, 0, 0; i < len(rs); i++ { + w = charWidth(rs[i]) + ps = append(ps, newPointWithAttrs(rs[i], lb.X+1+j, lb.Y, lb.LabelFgColor, lb.LabelBgColor)) + j += w } return ps diff --git a/chart.go b/chart.go index 12771b8..d752362 100644 --- a/chart.go +++ b/chart.go @@ -47,8 +47,8 @@ var rSingleBraille = [4]rune{'\u2880', '⠠', '⠐', '⠈'} type LineChart struct { Block Data []float64 - DataLabels []string - Mode string // braille | dot + DataLabels []string // if unset, the data indices will be used + Mode string // braille | dot DotStyle rune LineColor Attribute scale float64 // data span per cell on y-axis @@ -149,20 +149,22 @@ func (lc *LineChart) calcLabelX() { } s := str2runes(lc.DataLabels[l]) - if l+len(s) <= lc.axisXWidth { + w := strWidth(lc.DataLabels[l]) + if l+w <= lc.axisXWidth { lc.labelX = append(lc.labelX, s) } - l += (len(s) + lc.axisXLebelGap) // -1 needed - } else { + l += w + lc.axisXLebelGap + } else { // braille if 2*l >= len(lc.DataLabels) { break } s := str2runes(lc.DataLabels[2*l]) - if l+len(s) <= lc.axisXWidth { + w := strWidth(lc.DataLabels[2*l]) + if l+w <= lc.axisXWidth { lc.labelX = append(lc.labelX, s) } - l += (len(s) + lc.axisXLebelGap) // -1 needed + l += w + lc.axisXLebelGap } } @@ -199,6 +201,7 @@ func (lc *LineChart) calcLabelY() { } func (lc *LineChart) calcLayout() { + // set datalabels if it is not provided if lc.DataLabels == nil || len(lc.DataLabels) == 0 { lc.DataLabels = make([]string, len(lc.Data)) for i := range lc.Data { diff --git a/example/list.go b/example/list.go index 8727be9..9dcaeea 100644 --- a/example/list.go +++ b/example/list.go @@ -20,8 +20,8 @@ func main() { strs := []string{ "[0] github.com/gizak/termui", - "[1] editbox.go", - "[2] iterrupt.go", + "[1] 你好,世界", + "[2] こんにちは世界", "[3] keyboard.go", "[4] output.go", "[5] random_out.go", diff --git a/example/par.go b/example/par.go index 4540971..c56660e 100644 --- a/example/par.go +++ b/example/par.go @@ -24,11 +24,11 @@ func main() { par0.Y = 1 par0.HasBorder = false - par1 := termui.NewPar("Simple Text") + par1 := termui.NewPar("你好,世界。") par1.Height = 3 par1.Width = 17 par1.X = 20 - par1.Border.Label = "Label" + par1.Border.Label = "标签" par2 := termui.NewPar("Simple text\nwith label. It can be multilined with \\n or break automatically") par2.Height = 5 diff --git a/helper.go b/helper.go index 4b44843..dec705e 100644 --- a/helper.go +++ b/helper.go @@ -9,6 +9,7 @@ import rw "github.com/mattn/go-runewidth" /* ---------------Port from termbox-go --------------------- */ +// Attribute is printable cell's color and style. type Attribute uint16 const ( @@ -49,9 +50,16 @@ func trimStr2Runes(s string, w int) []rune { return []rune{} } sw := rw.StringWidth(s) - if sw+dotw >= w { - return []rune(rw.Truncate(s, w, "…")) - } else { - return []rune(rw.Truncate(s, w, "")) + if sw > w { + return []rune(rw.Truncate(s, w, dot)) } + return str2runes(s) //[]rune(rw.Truncate(s, w, "")) +} + +func strWidth(s string) int { + return rw.StringWidth(s) +} + +func charWidth(ch rune) int { + return rw.RuneWidth(ch) } diff --git a/helper_test.go b/helper_test.go index 6aa8d21..6d1a561 100644 --- a/helper_test.go +++ b/helper_test.go @@ -4,7 +4,11 @@ package termui -import "testing" +import ( + "testing" + + "github.com/davecgh/go-spew/spew" +) func TestStr2Rune(t *testing.T) { s := "你好,世界." @@ -13,3 +17,42 @@ func TestStr2Rune(t *testing.T) { t.Error() } } + +func TestWidth(t *testing.T) { + s0 := "つのだ☆HIRO" + s1 := "11111111111" + spew.Dump(s0) + spew.Dump(s1) + // above not align for setting East Asian Ambiguous to wide!! + + if strWidth(s0) != strWidth(s1) { + t.Error("str len failed") + } + + len1 := []rune{'a', '2', '&', '「', 'オ', '。'} //will false: 'ᆵ', 'ᄚ', 'ᄒ' + for _, v := range len1 { + if charWidth(v) != 1 { + t.Error("len1 failed") + } + } + + len2 := []rune{'漢', '字', '한', '자', '你', '好', 'だ', '。', '%', 's', 'E', 'ョ', '、', 'ヲ'} + for _, v := range len2 { + if charWidth(v) != 2 { + t.Error("len2 failed") + } + } +} + +func TestTrim(t *testing.T) { + s := "つのだ☆HIRO" + if string(trimStr2Runes(s, 10)) != "つのだ☆HI"+dot { + t.Error("trim failed") + } + if string(trimStr2Runes(s, 11)) != "つのだ☆HIRO" { + t.Error("avoid tail trim failed") + } + if string(trimStr2Runes(s, 15)) != "つのだ☆HIRO" { + t.Error("avoid trim failed") + } +} diff --git a/list.go b/list.go index 131c718..d670aa6 100644 --- a/list.go +++ b/list.go @@ -54,7 +54,8 @@ func (l *List) Buffer() []Point { rs := str2runes(strings.Join(l.Items, "\n")) i, j, k := 0, 0, 0 for i < l.innerHeight && k < len(rs) { - if rs[k] == '\n' || j == l.innerWidth { + w := charWidth(rs[k]) + if rs[k] == '\n' || j+w > l.innerWidth { i++ j = 0 if rs[k] == '\n' { @@ -85,6 +86,7 @@ func (l *List) Buffer() []Point { j := 0 for _, vv := range rs { + w := charWidth(vv) p := Point{} p.X = l.innerX + j p.Y = l.innerY + i @@ -94,7 +96,7 @@ func (l *List) Buffer() []Point { p.Fg = l.ItemFgColor ps = append(ps, p) - j++ + j += w } } } diff --git a/p.go b/p.go index fe03ab0..22332ed 100644 --- a/p.go +++ b/p.go @@ -34,9 +34,12 @@ func (p *Par) Buffer() []Point { rs := str2runes(p.Text) i, j, k := 0, 0, 0 for i < p.innerHeight && k < len(rs) { - if rs[k] == '\n' || j == p.innerWidth { + // the width of char is about to print + w := charWidth(rs[k]) + + if rs[k] == '\n' || j+w > p.innerWidth { i++ - j = 0 + j = 0 // set x = 0 if rs[k] == '\n' { k++ } @@ -62,7 +65,7 @@ func (p *Par) Buffer() []Point { ps = append(ps, pi) k++ - j++ + j += w } return ps } diff --git a/sparkline.go b/sparkline.go index db87f39..2a8cc7b 100644 --- a/sparkline.go +++ b/sparkline.go @@ -111,7 +111,9 @@ func (sl *Sparklines) Buffer() []Point { if l.Title != "" { rs := trimStr2Runes(l.Title, sl.innerWidth) - for oftX, v := range rs { + oftX := 0 + for _, v := range rs { + w := charWidth(v) p := Point{} p.Ch = v p.Fg = l.TitleColor @@ -119,6 +121,7 @@ func (sl *Sparklines) Buffer() []Point { p.X = sl.innerX + oftX p.Y = sl.innerY + oftY ps = append(ps, p) + oftX += w } } @@ -130,7 +133,7 @@ func (sl *Sparklines) Buffer() []Point { p := Point{} p.X = sl.innerX + j p.Y = sl.innerY + oftY + l.Height - jj - p.Ch = ' ' //sparks[7] + p.Ch = ' ' // => sparks[7] p.Bg = l.LineColor //p.Bg = sl.BgColor ps = append(ps, p)