diff --git a/internal/ui/format.go b/internal/ui/format.go index d2e0a4d2b..de650607d 100644 --- a/internal/ui/format.go +++ b/internal/ui/format.go @@ -8,6 +8,8 @@ import ( "math/bits" "strconv" "time" + + "golang.org/x/text/width" ) func FormatBytes(c uint64) string { @@ -105,3 +107,24 @@ func ToJSONString(status interface{}) string { } return buf.String() } + +// TerminalDisplayWidth returns the number of terminal cells needed to display s +func TerminalDisplayWidth(s string) int { + width := 0 + for _, r := range s { + width += terminalDisplayRuneWidth(r) + } + + return width +} + +func terminalDisplayRuneWidth(r rune) int { + switch width.LookupRune(r).Kind() { + case width.EastAsianWide, width.EastAsianFullwidth: + return 2 + case width.EastAsianNarrow, width.EastAsianHalfwidth, width.EastAsianAmbiguous, width.Neutral: + return 1 + default: + return 0 + } +} diff --git a/internal/ui/format_test.go b/internal/ui/format_test.go index 4223d4e20..d595026c4 100644 --- a/internal/ui/format_test.go +++ b/internal/ui/format_test.go @@ -84,3 +84,21 @@ func TestParseBytesInvalid(t *testing.T) { test.Equals(t, int64(0), v) } } + +func TestTerminalDisplayWidth(t *testing.T) { + for _, c := range []struct { + input string + want int + }{ + {"foo", 3}, + {"aéb", 3}, + {"ab", 3}, + {"a’b", 3}, + {"aあb", 4}, + } { + if got := TerminalDisplayWidth(c.input); got != c.want { + t.Errorf("wrong display width for '%s', want %d, got %d", c.input, c.want, got) + } + } + +} diff --git a/internal/ui/table/table.go b/internal/ui/table/table.go index c3ae47f54..ae09063be 100644 --- a/internal/ui/table/table.go +++ b/internal/ui/table/table.go @@ -6,6 +6,8 @@ import ( "strings" "text/template" + + "github.com/restic/restic/internal/ui" ) // Table contains data for a table to be printed. @@ -89,7 +91,7 @@ func printLine(w io.Writer, print func(io.Writer, string) error, sep string, dat } // apply padding - pad := widths[fieldNum] - len(v) + pad := widths[fieldNum] - ui.TerminalDisplayWidth(v) if pad > 0 { v += strings.Repeat(" ", pad) } @@ -139,16 +141,16 @@ func (t *Table) Write(w io.Writer) error { columnWidths := make([]int, columns) for i, desc := range t.columns { for _, line := range strings.Split(desc, "\n") { - if columnWidths[i] < len(line) { - columnWidths[i] = len(desc) + if columnWidths[i] < ui.TerminalDisplayWidth(line) { + columnWidths[i] = ui.TerminalDisplayWidth(desc) } } } for _, line := range lines { for i, content := range line { for _, l := range strings.Split(content, "\n") { - if columnWidths[i] < len(l) { - columnWidths[i] = len(l) + if columnWidths[i] < ui.TerminalDisplayWidth(l) { + columnWidths[i] = ui.TerminalDisplayWidth(l) } } } @@ -159,7 +161,7 @@ func (t *Table) Write(w io.Writer) error { for _, width := range columnWidths { totalWidth += width } - totalWidth += (columns - 1) * len(t.CellSeparator) + totalWidth += (columns - 1) * ui.TerminalDisplayWidth(t.CellSeparator) // write header if len(t.columns) > 0 { diff --git a/internal/ui/table/table_test.go b/internal/ui/table/table_test.go index db116bbc5..7a94b7f9b 100644 --- a/internal/ui/table/table_test.go +++ b/internal/ui/table/table_test.go @@ -126,7 +126,7 @@ foo 2018-08-19 22:22:22 xxx other /home/user/other Time string Tags, Dirs []string } - table.AddRow(data{"foo", "2018-08-19 22:22:22", []string{"work", "go"}, []string{"/home/user/work", "/home/user/go"}}) + table.AddRow(data{"foo", "2018-08-19 22:22:22", []string{"work", "go’s"}, []string{"/home/user/work", "/home/user/go"}}) table.AddRow(data{"foo", "2018-08-19 22:22:22", []string{"other"}, []string{"/home/user/other"}}) table.AddRow(data{"foo", "2018-08-19 22:22:22", []string{"other", "bar"}, []string{"/home/user/other"}}) return table @@ -135,7 +135,7 @@ foo 2018-08-19 22:22:22 xxx other /home/user/other host name time zz tags dirs ------------------------------------------------------------ foo 2018-08-19 22:22:22 xxx work /home/user/work - go /home/user/go + go’s /home/user/go foo 2018-08-19 22:22:22 xxx other /home/user/other foo 2018-08-19 22:22:22 xxx other /home/user/other bar