initial commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
table_header_test.go
|
7
col_spec.go
Normal file
7
col_spec.go
Normal file
@ -0,0 +1,7 @@
|
||||
package tprint
|
||||
|
||||
type ColSpec struct {
|
||||
Name string
|
||||
MaxWidth int
|
||||
MaxHeight int
|
||||
}
|
17
go.mod
Normal file
17
go.mod
Normal file
@ -0,0 +1,17 @@
|
||||
module git.tordarus.net/tordarus/tprint
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
git.tordarus.net/tordarus/gmath v0.0.7
|
||||
git.tordarus.net/tordarus/slices v0.0.14
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/mattn/go-runewidth v0.0.16
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
)
|
19
go.sum
Normal file
19
go.sum
Normal file
@ -0,0 +1,19 @@
|
||||
git.tordarus.net/tordarus/gmath v0.0.7 h1:tR48idt9AUL0r556ww3ZxByTKJEr6NWCTlhl2ihzYxQ=
|
||||
git.tordarus.net/tordarus/gmath v0.0.7/go.mod h1:mO7aPlvNrGVE9UFXEuuACjZgMDsM63l3OcQy6xSQnoE=
|
||||
git.tordarus.net/tordarus/slices v0.0.14 h1:Jy1VRMs777WewJ7mxTgjyQIMm/Zr+co18/XoQ01YZ3A=
|
||||
git.tordarus.net/tordarus/slices v0.0.14/go.mod h1:RgE7A1aSAezIvPUgcbUuMHu0q4xGKoRevT+DC0eJmwI=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
6
options.go
Normal file
6
options.go
Normal file
@ -0,0 +1,6 @@
|
||||
package tprint
|
||||
|
||||
type TableOptions struct {
|
||||
Header bool
|
||||
Border bool
|
||||
}
|
168
table.go
Normal file
168
table.go
Normal file
@ -0,0 +1,168 @@
|
||||
package tprint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.tordarus.net/tordarus/gmath"
|
||||
"git.tordarus.net/tordarus/slices"
|
||||
)
|
||||
|
||||
type Table struct {
|
||||
head [][]string
|
||||
|
||||
rowSep bool
|
||||
headheight int
|
||||
maxcols int
|
||||
|
||||
colwidth []int
|
||||
rowheight []int
|
||||
|
||||
data [][][]string
|
||||
}
|
||||
|
||||
func NewTable(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(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(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(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) StringNoHead() 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')
|
||||
|
||||
for i, row := range t.data {
|
||||
t.printRow(b, row, t.rowheight[i])
|
||||
if t.rowSep && i < len(t.data)-1 {
|
||||
t.addNextCellLine(b)
|
||||
}
|
||||
}
|
||||
|
||||
t.addLastCellLine(b)
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
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')
|
||||
|
||||
t.printRow(b, t.head, t.headheight)
|
||||
|
||||
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')
|
||||
|
||||
for i, row := range t.data {
|
||||
t.printRow(b, 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, 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(fmt.Sprint(padStringRight(line, ' ', t.colwidth[colIndex])))
|
||||
if colIndex < t.maxcols-1 {
|
||||
b.WriteRune('│')
|
||||
}
|
||||
}
|
||||
b.WriteString("┃\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.WriteString("┛\n")
|
||||
}
|
44
table_header.go
Normal file
44
table_header.go
Normal file
@ -0,0 +1,44 @@
|
||||
package tprint
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.tordarus.net/tordarus/slices"
|
||||
)
|
||||
|
||||
func FormatHeaderTable(header string, table *Table) string {
|
||||
return formatHeaderTableStr(header, table.String())
|
||||
}
|
||||
|
||||
func FormatHeaderTableNoHead(header string, table *Table) string {
|
||||
return formatHeaderTableStr(header, table.StringNoHead())
|
||||
}
|
||||
|
||||
// TODO formatHeaderTableStr is a quick hack
|
||||
// a better solution would be to support nested tables.
|
||||
// in that case, a header table is just a table with a singe col, single header row and single data row
|
||||
|
||||
func formatHeaderTableStr(header string, tab string) string {
|
||||
b := new(strings.Builder)
|
||||
|
||||
splits := strings.Split(tab, "\n")
|
||||
tabwidth := strLen(splits[0])
|
||||
|
||||
b.WriteRune('┏')
|
||||
b.WriteString(strings.Repeat("━", tabwidth-2))
|
||||
b.WriteString("┓\n┃")
|
||||
b.WriteString(padStringCenter(header, ' ', tabwidth-2))
|
||||
b.WriteString("┃\n")
|
||||
|
||||
b.WriteRune('┣')
|
||||
b.WriteString(splits[0][3 : len(splits[0])-3])
|
||||
b.WriteString("┫\n")
|
||||
|
||||
slices.Each(splits[1:], func(s string) {
|
||||
b.WriteString(s)
|
||||
b.WriteRune('\n')
|
||||
})
|
||||
|
||||
ret := b.String()
|
||||
return ret[:len(ret)-1]
|
||||
}
|
72
utils.go
Normal file
72
utils.go
Normal file
@ -0,0 +1,72 @@
|
||||
package tprint
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
func Search[T any](slice []T, cmp func(a, b T) T) T {
|
||||
best := *new(T)
|
||||
for _, v := range slice {
|
||||
best = cmp(v, best)
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
for i := 0; i < len(runes); i++ {
|
||||
rn := runes[i]
|
||||
|
||||
// skip control sequences
|
||||
if unicode.IsControl(rn) {
|
||||
for j := i; j < len(runes)-1 && runes[j] != 'm'; j++ {
|
||||
i = j
|
||||
}
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if rn <= 0xFF || rn >= '─' && rn <= '╿' {
|
||||
l++
|
||||
} else {
|
||||
l += runewidth.RuneWidth(rn)
|
||||
}
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func padStringRight(str string, pad rune, length int) string {
|
||||
padding := length - strLen(str)
|
||||
return str + strings.Repeat(string(pad), padding)
|
||||
}
|
||||
|
||||
func padStringLeft(str string, pad rune, length int) string {
|
||||
padding := length - strLen(str)
|
||||
return strings.Repeat(string(pad), padding) + str
|
||||
}
|
||||
|
||||
func padStringCenter(str string, pad rune, length int) string {
|
||||
l := strLen(str)
|
||||
return padStringLeft(padStringRight(str, pad, (length-l)/2+l), pad, length)
|
||||
}
|
Reference in New Issue
Block a user