From e9de9684f4da9638450c2e5088a365e020097ea3 Mon Sep 17 00:00:00 2001 From: Florian Thoma Date: Wed, 5 Jun 2024 09:33:15 +0200 Subject: [PATCH 1/3] 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 From edd3e214c2337ae69758c46506a1e94c81b67d68 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Fri, 7 Jun 2024 21:44:49 +0200 Subject: [PATCH 2/3] ui/table: fix width calculation of multi-line column headers --- internal/ui/table/table.go | 2 +- internal/ui/table/table_test.go | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/internal/ui/table/table.go b/internal/ui/table/table.go index ae09063be..0423ddb48 100644 --- a/internal/ui/table/table.go +++ b/internal/ui/table/table.go @@ -142,7 +142,7 @@ func (t *Table) Write(w io.Writer) error { for i, desc := range t.columns { for _, line := range strings.Split(desc, "\n") { if columnWidths[i] < ui.TerminalDisplayWidth(line) { - columnWidths[i] = ui.TerminalDisplayWidth(desc) + columnWidths[i] = ui.TerminalDisplayWidth(line) } } } diff --git a/internal/ui/table/table_test.go b/internal/ui/table/table_test.go index 7a94b7f9b..2902860b9 100644 --- a/internal/ui/table/table_test.go +++ b/internal/ui/table/table_test.go @@ -29,6 +29,21 @@ first column ---------------------- data: first data field ---------------------- +`, + }, + { + func(t testing.TB) *Table { + table := New() + table.AddColumn("first\ncolumn", "{{.First}}") + table.AddRow(struct{ First string }{"data"}) + return table + }, + ` +first +column +------ +data +------ `, }, { From 7cd324fe2638a99c4fc1853782bcdc838eacf8a2 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Fri, 7 Jun 2024 21:45:40 +0200 Subject: [PATCH 3/3] ui/table: avoid duplicate table cell width calculation --- internal/ui/table/table.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/ui/table/table.go b/internal/ui/table/table.go index 0423ddb48..1c535cadb 100644 --- a/internal/ui/table/table.go +++ b/internal/ui/table/table.go @@ -141,16 +141,18 @@ 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] < ui.TerminalDisplayWidth(line) { - columnWidths[i] = ui.TerminalDisplayWidth(line) + width := ui.TerminalDisplayWidth(line) + if columnWidths[i] < width { + columnWidths[i] = width } } } for _, line := range lines { for i, content := range line { for _, l := range strings.Split(content, "\n") { - if columnWidths[i] < ui.TerminalDisplayWidth(l) { - columnWidths[i] = ui.TerminalDisplayWidth(l) + width := ui.TerminalDisplayWidth(l) + if columnWidths[i] < width { + columnWidths[i] = width } } }