mirror of https://github.com/restic/restic.git
ui/table: Add small package for writing tables
This commit is contained in:
parent
d708d607fa
commit
12246969db
|
@ -0,0 +1,206 @@
|
||||||
|
package table
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Table contains data for a table to be printed.
|
||||||
|
type Table struct {
|
||||||
|
columns []string
|
||||||
|
templates []*template.Template
|
||||||
|
data []interface{}
|
||||||
|
footer []string
|
||||||
|
|
||||||
|
CellSeparator string
|
||||||
|
PrintHeader func(io.Writer, string) error
|
||||||
|
PrintSeparator func(io.Writer, string) error
|
||||||
|
PrintData func(io.Writer, int, string) error
|
||||||
|
PrintFooter func(io.Writer, string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
var funcmap = template.FuncMap{
|
||||||
|
"join": strings.Join,
|
||||||
|
}
|
||||||
|
|
||||||
|
// New initializes a new Table
|
||||||
|
func New() *Table {
|
||||||
|
p := func(w io.Writer, s string) error {
|
||||||
|
_, err := w.Write(append([]byte(s), '\n'))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return &Table{
|
||||||
|
CellSeparator: " ",
|
||||||
|
PrintHeader: p,
|
||||||
|
PrintSeparator: p,
|
||||||
|
PrintData: func(w io.Writer, _ int, s string) error {
|
||||||
|
return p(w, s)
|
||||||
|
},
|
||||||
|
PrintFooter: p,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddColumn adds a new header field with the header and format, which is
|
||||||
|
// expected to be template string compatible with text/template. When compiling
|
||||||
|
// the format fails, AddColumn panics.
|
||||||
|
func (t *Table) AddColumn(header, format string) {
|
||||||
|
t.columns = append(t.columns, header)
|
||||||
|
tmpl, err := template.New("template for " + header).Funcs(funcmap).Parse(format)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.templates = append(t.templates, tmpl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddRow adds a new row to the table, which is filled with data.
|
||||||
|
func (t *Table) AddRow(data interface{}) {
|
||||||
|
t.data = append(t.data, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFooter prints line after the table
|
||||||
|
func (t *Table) AddFooter(line string) {
|
||||||
|
t.footer = append(t.footer, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printLine(w io.Writer, print func(io.Writer, string) error, sep string, data []string, widths []int) error {
|
||||||
|
var fields [][]string
|
||||||
|
|
||||||
|
maxLines := 1
|
||||||
|
for _, d := range data {
|
||||||
|
lines := strings.Split(d, "\n")
|
||||||
|
if len(lines) > maxLines {
|
||||||
|
maxLines = len(lines)
|
||||||
|
}
|
||||||
|
fields = append(fields, lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < maxLines; i++ {
|
||||||
|
var s string
|
||||||
|
|
||||||
|
for fieldNum, lines := range fields {
|
||||||
|
var v string
|
||||||
|
|
||||||
|
if i < len(lines) {
|
||||||
|
v += lines[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply padding
|
||||||
|
pad := widths[fieldNum] - len(v)
|
||||||
|
if pad > 0 {
|
||||||
|
v += strings.Repeat(" ", pad)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fieldNum > 0 {
|
||||||
|
v = sep + v
|
||||||
|
}
|
||||||
|
|
||||||
|
s += v
|
||||||
|
}
|
||||||
|
|
||||||
|
err := print(w, strings.TrimRight(s, " "))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write prints the table to w.
|
||||||
|
func (t *Table) Write(w io.Writer) error {
|
||||||
|
columns := len(t.templates)
|
||||||
|
if columns == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// collect all data fields from all columns
|
||||||
|
lines := make([][]string, 0, len(t.data))
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
|
||||||
|
for _, data := range t.data {
|
||||||
|
row := make([]string, 0, len(t.templates))
|
||||||
|
for _, tmpl := range t.templates {
|
||||||
|
err := tmpl.Execute(buf, data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
row = append(row, string(buf.Bytes()))
|
||||||
|
buf.Reset()
|
||||||
|
}
|
||||||
|
lines = append(lines, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
// find max width for each cell
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate the total width of the table
|
||||||
|
totalWidth := 0
|
||||||
|
for _, width := range columnWidths {
|
||||||
|
totalWidth += width
|
||||||
|
}
|
||||||
|
totalWidth += (columns - 1) * len(t.CellSeparator)
|
||||||
|
|
||||||
|
// write header
|
||||||
|
if len(t.columns) > 0 {
|
||||||
|
err := printLine(w, t.PrintHeader, t.CellSeparator, t.columns, columnWidths)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw separation line
|
||||||
|
err = t.PrintSeparator(w, strings.Repeat("-", totalWidth))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write all the lines
|
||||||
|
for i, line := range lines {
|
||||||
|
print := func(w io.Writer, s string) error {
|
||||||
|
return t.PrintData(w, i, s)
|
||||||
|
}
|
||||||
|
err := printLine(w, print, t.CellSeparator, line, columnWidths)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw separation line
|
||||||
|
err := t.PrintSeparator(w, strings.Repeat("-", totalWidth))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(t.footer) > 0 {
|
||||||
|
// write the footer
|
||||||
|
for _, line := range t.footer {
|
||||||
|
err := t.PrintFooter(w, line)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,162 @@
|
||||||
|
package table
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTable(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
create func(t testing.TB) *Table
|
||||||
|
output string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
func(t testing.TB) *Table {
|
||||||
|
return New()
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
func(t testing.TB) *Table {
|
||||||
|
table := New()
|
||||||
|
table.AddColumn("first column", "data: {{.First}}")
|
||||||
|
table.AddRow(struct{ First string }{"first data field"})
|
||||||
|
return table
|
||||||
|
},
|
||||||
|
`
|
||||||
|
first column
|
||||||
|
----------------------
|
||||||
|
data: first data field
|
||||||
|
----------------------
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
func(t testing.TB) *Table {
|
||||||
|
table := New()
|
||||||
|
table.AddColumn(" first column ", "data: {{.First}}")
|
||||||
|
table.AddRow(struct{ First string }{"d"})
|
||||||
|
return table
|
||||||
|
},
|
||||||
|
`
|
||||||
|
first column
|
||||||
|
----------------
|
||||||
|
data: d
|
||||||
|
----------------
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
func(t testing.TB) *Table {
|
||||||
|
table := New()
|
||||||
|
table.AddColumn("first column", "data: {{.First}}")
|
||||||
|
table.AddRow(struct{ First string }{"first data field"})
|
||||||
|
table.AddRow(struct{ First string }{"second data field"})
|
||||||
|
table.AddFooter("footer1")
|
||||||
|
table.AddFooter("footer2")
|
||||||
|
return table
|
||||||
|
},
|
||||||
|
`
|
||||||
|
first column
|
||||||
|
-----------------------
|
||||||
|
data: first data field
|
||||||
|
data: second data field
|
||||||
|
-----------------------
|
||||||
|
footer1
|
||||||
|
footer2
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
func(t testing.TB) *Table {
|
||||||
|
table := New()
|
||||||
|
table.AddColumn(" first name", `{{printf "%12s" .FirstName}}`)
|
||||||
|
table.AddColumn("last name", "{{.LastName}}")
|
||||||
|
table.AddRow(struct{ FirstName, LastName string }{"firstname", "lastname"})
|
||||||
|
table.AddRow(struct{ FirstName, LastName string }{"John", "Doe"})
|
||||||
|
table.AddRow(struct{ FirstName, LastName string }{"Johann", "van den Berjen"})
|
||||||
|
return table
|
||||||
|
},
|
||||||
|
`
|
||||||
|
first name last name
|
||||||
|
----------------------------
|
||||||
|
firstname lastname
|
||||||
|
John Doe
|
||||||
|
Johann van den Berjen
|
||||||
|
----------------------------
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
func(t testing.TB) *Table {
|
||||||
|
table := New()
|
||||||
|
table.AddColumn("host name", `{{.Host}}`)
|
||||||
|
table.AddColumn("time", `{{.Time}}`)
|
||||||
|
table.AddColumn("zz", "xxx")
|
||||||
|
table.AddColumn("tags", `{{join .Tags ","}}`)
|
||||||
|
table.AddColumn("dirs", `{{join .Dirs ","}}`)
|
||||||
|
|
||||||
|
type data struct {
|
||||||
|
Host string
|
||||||
|
Time string
|
||||||
|
Tags, Dirs []string
|
||||||
|
}
|
||||||
|
table.AddRow(data{"foo", "2018-08-19 22:22:22", []string{"work"}, []string{"/home/user/work"}})
|
||||||
|
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"}, []string{"/home/user/other"}})
|
||||||
|
return table
|
||||||
|
},
|
||||||
|
`
|
||||||
|
host name time zz tags dirs
|
||||||
|
------------------------------------------------------------
|
||||||
|
foo 2018-08-19 22:22:22 xxx work /home/user/work
|
||||||
|
foo 2018-08-19 22:22:22 xxx other /home/user/other
|
||||||
|
foo 2018-08-19 22:22:22 xxx other /home/user/other
|
||||||
|
------------------------------------------------------------
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
func(t testing.TB) *Table {
|
||||||
|
table := New()
|
||||||
|
table.AddColumn("host name", `{{.Host}}`)
|
||||||
|
table.AddColumn("time", `{{.Time}}`)
|
||||||
|
table.AddColumn("zz", "xxx")
|
||||||
|
table.AddColumn("tags", `{{join .Tags "\n"}}`)
|
||||||
|
table.AddColumn("dirs", `{{join .Dirs "\n"}}`)
|
||||||
|
|
||||||
|
type data struct {
|
||||||
|
Host string
|
||||||
|
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{"other"}, []string{"/home/user/other"}})
|
||||||
|
table.AddRow(data{"foo", "2018-08-19 22:22:22", []string{"other", "bar"}, []string{"/home/user/other"}})
|
||||||
|
return table
|
||||||
|
},
|
||||||
|
`
|
||||||
|
host name time zz tags dirs
|
||||||
|
------------------------------------------------------------
|
||||||
|
foo 2018-08-19 22:22:22 xxx work /home/user/work
|
||||||
|
go /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
|
||||||
|
------------------------------------------------------------
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run("", func(t *testing.T) {
|
||||||
|
table := test.create(t)
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
err := table.Write(buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := strings.TrimLeft(test.output, "\n")
|
||||||
|
if string(buf.Bytes()) != want {
|
||||||
|
t.Errorf("wrong output\n---- want ---\n%s\n---- got ---\n%s\n-------\n", want, buf.Bytes())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue