commit 8e9954514b0a556331438baeac4edd99c71eb773 Author: Timon Ringwald Date: Thu Aug 25 16:00:13 2022 +0200 initial commit diff --git a/col_spec.go b/col_spec.go new file mode 100644 index 0000000..708e738 --- /dev/null +++ b/col_spec.go @@ -0,0 +1,7 @@ +package tableprint + +type ColSpec struct { + Name string + MaxWidth int + MaxHeight int +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1f63871 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module git.milar.in/milarin/tprint + +go 1.19 + +require ( + git.milar.in/milarin/gmath v0.0.2 + git.milar.in/milarin/slices v0.0.3 + github.com/fatih/color v1.13.0 +) + +require ( + git.milar.in/milarin/channel v0.0.9 // indirect + github.com/mattn/go-colorable v0.1.9 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1bbaafe --- /dev/null +++ b/go.sum @@ -0,0 +1,17 @@ +git.milar.in/milarin/channel v0.0.9 h1:vYJnXOaIn/+mng1+8CyepQgDfRse2s4FSLNc7zv85To= +git.milar.in/milarin/channel v0.0.9/go.mod h1:We83LTI8S7u7II3pD+A2ChCDWJfCkcBUCUqii9HjTtM= +git.milar.in/milarin/gmath v0.0.2 h1:avz+75f8XqAYA1wEB6kis0R5xvRuepBKTqBuJBjh6Yw= +git.milar.in/milarin/gmath v0.0.2/go.mod h1:HDLftG5RLpiNGKiIWh+O2G1PYkNzyLDADO8Cd/1abiE= +git.milar.in/milarin/slices v0.0.3 h1:kzaLrE/G4rO2DQq3nVk2TYbuqOsiauLHClVUpgSZM8s= +git.milar.in/milarin/slices v0.0.3/go.mod h1:XRNfE99aNKeaPOY1phjOlpIQqeGCW1LOqqh8UHS+vAk= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/table.go b/table.go new file mode 100644 index 0000000..9732581 --- /dev/null +++ b/table.go @@ -0,0 +1,141 @@ +package tableprint + +import ( + "fmt" + "strings" + + "git.milar.in/milarin/gmath" + "git.milar.in/milarin/slices" + "github.com/fatih/color" +) + +type Table struct { + head [][]string + + rowSep bool + headheight int + maxcols int + + colwidth []int + rowheight []int + + data [][][]string +} + +func NewDynamicTable(head ...string) *Table { + thead := slices.Map(head, func(s string) []string { return strings.Split(s, "\n") }) + return &Table{ + head: thead, + colwidth: slices.Map(head, func(s string) int { return strLen(s) }), + headheight: len(slices.Search(thead, maxLengthSlice[string])), + rowheight: []int{}, + maxcols: len(thead), + } +} + +func (t *Table) AddRow(row ...interface{}) { + irow := slices.Map(row, func(v interface{}) []string { return strings.Split(fmt.Sprint(v), "\n") }) + t.data = append(t.data, irow) + + rowheight := len(slices.Search(irow, maxLengthSlice[string])) + t.rowheight = append(t.rowheight, rowheight) + if rowheight > 1 { + t.rowSep = true + } + + t.maxcols = gmath.Max(t.maxcols, len(irow)) + + for i, cell := range irow { + maxLineLength := strLen(slices.Search(cell, maxLengthStr)) + if i < len(t.colwidth) { + t.colwidth[i] = gmath.Max(t.colwidth[i], maxLineLength) + } else { + t.colwidth = append(t.colwidth, maxLineLength) + } + + if i >= len(t.head) { + headname := "unknown column" + t.head = append(t.head, []string{headname}) + t.colwidth[i] = gmath.Max(t.colwidth[i], strLen(headname)) + } + } +} + +func (t *Table) String() string { + b := new(strings.Builder) + + b.WriteRune('┌') + for i, colwidth := range t.colwidth { + b.WriteString(strings.Repeat("─", colwidth)) + if i < len(t.colwidth)-1 { + b.WriteRune('┬') + } + } + b.WriteRune('┐') + b.WriteRune('\n') + + bold := color.New(color.Bold) + t.printRow(b, bold.Sprint, t.head, t.headheight) + + if len(t.data) > 0 { + t.addNextCellLine(b) + } else { + t.addLastCellLine(b) + return b.String() + } + + for i, row := range t.data { + t.printRow(b, fmt.Sprint, row, t.rowheight[i]) + if t.rowSep && i < len(t.data)-1 { + t.addNextCellLine(b) + } + } + + t.addLastCellLine(b) + + return b.String() +} + +func (t *Table) printRow(b *strings.Builder, printFunc func(...interface{}) string, rowData [][]string, rowHeight int) { + for lineIndex := 0; lineIndex < rowHeight; lineIndex++ { + b.WriteRune('│') + for colIndex := 0; colIndex < t.maxcols; colIndex++ { + line := "" + + if colIndex < len(rowData) { + cell := rowData[colIndex] + if lineIndex < len(cell) { + line = cell[lineIndex] + } + } + + b.WriteString(printFunc(padStringRight(line, ' ', t.colwidth[colIndex]))) + b.WriteRune('│') + } + b.WriteRune('\n') + } +} + +func (t *Table) addNextCellLine(b *strings.Builder) { + b.WriteRune('├') + for i, colwidth := range t.colwidth { + b.WriteString(strings.Repeat("─", colwidth)) + if i < len(t.colwidth)-1 { + b.WriteRune('┼') + } + } + b.WriteRune('┤') + b.WriteRune('\n') +} + +func (t *Table) addLastCellLine(b *strings.Builder) { + b.WriteRune('└') + for i, colwidth := range t.colwidth { + b.WriteString(strings.Repeat("─", colwidth)) + if i < len(t.colwidth)-1 { + b.WriteRune('┴') + } + } + b.WriteRune('┘') + b.WriteRune('\n') +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..2ad3de7 --- /dev/null +++ b/utils.go @@ -0,0 +1,48 @@ +package tableprint + +import ( + "strings" + "unicode" +) + +func maxLengthStr(a, b string) string { + if strLen(a) > strLen(b) { + return a + } + return b +} + +func maxLengthSlice[T any](a, b []T) []T { + if len(a) > len(b) { + return a + } + return b +} + +func strLen(str string) int { + l := 0 + + runes := []rune(str) + colored := false + + for i := 0; i < len(runes); i++ { + if unicode.IsControl(runes[i]) { + if colored { + i += 3 + } else { + i += 4 + } + colored = !colored + continue + } + + l++ + } + + return l +} + +func padStringRight(str string, pad rune, length int) string { + padding := length - strLen(str) + return str + strings.Repeat(string(pad), padding) +}