From e9de9684f4da9638450c2e5088a365e020097ea3 Mon Sep 17 00:00:00 2001 From: Florian Thoma Date: Wed, 5 Jun 2024 09:33:15 +0200 Subject: [PATCH] Use character display width for table padding Using len(...) for table cell padding produced wrong results for unicode chracters leading to misaligned tables. Implementation changed to take the actual terminal display width into consideration. --- internal/ui/format.go | 23 +++++++++++++++++++++++ internal/ui/format_test.go | 18 ++++++++++++++++++ internal/ui/table/table.go | 14 ++++++++------ internal/ui/table/table_test.go | 4 ++-- 4 files changed, 51 insertions(+), 8 deletions(-) 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