Skip to content

Commit

Permalink
feat: Add vertical header alignment option (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
liamg committed Jul 20, 2022
1 parent f6d33d0 commit 988842c
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 37 deletions.
2 changes: 2 additions & 0 deletions align.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ const (
AlignLeft Alignment = iota
AlignRight
AlignCenter
AlignBottom
AlignTop
)
105 changes: 68 additions & 37 deletions table.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,29 @@ import (

// Table holds information required to render a table to the terminal
type Table struct {
w io.Writer
data [][]string
formatted []iRow
headers [][]string
footers [][]string
alignments []Alignment
headerAlignments []Alignment
footerAlignments []Alignment
borders Borders
lineStyle Style
dividers Dividers
maxColumnWidth int
padding int
cursorStyle Style
rowLines bool
autoMerge bool
autoMergeHeaders bool
headerStyle Style
headerColspans map[int][]int
contentColspans map[int][]int
footerColspans map[int][]int
availableWidth int
w io.Writer
data [][]string
formatted []iRow
headers [][]string
footers [][]string
alignments []Alignment
headerAlignments []Alignment
footerAlignments []Alignment
borders Borders
lineStyle Style
dividers Dividers
maxColumnWidth int
padding int
cursorStyle Style
rowLines bool
autoMerge bool
autoMergeHeaders bool
headerStyle Style
headerColspans map[int][]int
contentColspans map[int][]int
footerColspans map[int][]int
availableWidth int
headerVerticalAlign Alignment
}

type iRow struct {
Expand All @@ -55,6 +56,7 @@ type iCol struct {
last bool
height int
mergeAbove bool
mergeBelow bool
alignment Alignment
}

Expand Down Expand Up @@ -101,16 +103,17 @@ func New(w io.Writer) *Table {
Right: true,
Bottom: true,
},
lineStyle: StyleNormal,
dividers: UnicodeDividers,
maxColumnWidth: 60,
padding: 1,
rowLines: true,
autoMerge: false,
headerColspans: make(map[int][]int),
contentColspans: make(map[int][]int),
footerColspans: make(map[int][]int),
availableWidth: availableWidth,
lineStyle: StyleNormal,
dividers: UnicodeDividers,
maxColumnWidth: 60,
padding: 1,
rowLines: true,
autoMerge: false,
headerColspans: make(map[int][]int),
contentColspans: make(map[int][]int),
footerColspans: make(map[int][]int),
availableWidth: availableWidth,
headerVerticalAlign: AlignTop,
}
}

Expand Down Expand Up @@ -176,6 +179,10 @@ func (t *Table) SetHeaderAlignment(columns ...Alignment) {
t.headerAlignments = columns
}

func (t *Table) SetHeaderVerticalAlignment(a Alignment) {
t.headerVerticalAlign = a
}

// SetFooterAlignment sets the alignment of each footer. Should be specified for each footer in the supplied data.
// Default alignment for footers is AlignCenter
func (t *Table) SetFooterAlignment(columns ...Alignment) {
Expand Down Expand Up @@ -453,9 +460,7 @@ func (t *Table) formatContent(formatted []iRow) []iRow {
}
// ensure all cols have the same number of lines for a given row
for c, col := range row.cols {
for len(col.lines) < maxLines {
col.lines = append(col.lines, newANSI(""))
}
col.lines = t.alignVertically(col.lines, AlignTop, maxLines)
col.height = len(col.lines)
formatted[r].cols[c] = col
}
Expand Down Expand Up @@ -496,6 +501,20 @@ func (t *Table) formatContent(formatted []iRow) []iRow {
return t.applyColSpans(formatted)
}

func (t *Table) alignVertically(lines []ansiBlob, alignment Alignment, maxLines int) []ansiBlob {
switch alignment {
case AlignBottom:
for len(lines) < maxLines {
lines = append([]ansiBlob{newANSI("")}, lines...)
}
default:
for len(lines) < maxLines {
lines = append(lines, newANSI(""))
}
}
return lines
}

// e.g. 5 rows, 2nd with span 5, input 7 returns 3rd row
func (t *Table) getRealIndex(row iRow, index int) int {
var relative int
Expand Down Expand Up @@ -638,6 +657,7 @@ func (t *Table) mergeContent(formatted []iRow) []iRow {

columnCount := t.calcColumnWidth(0, formatted[0])
lastValues := make([]string, columnCount)
lastIndexes := make([]int, columnCount)

// flag cols as mergeAbove where content matches and is non-empty
for c := 0; c < columnCount; c++ {
Expand All @@ -657,7 +677,17 @@ func (t *Table) mergeContent(formatted []iRow) []iRow {
current := row.cols[c].original
merge := current == lastValues[relativeIndex] && strings.TrimSpace(current) != ""
row.cols[c].mergeAbove = merge && allowed
if merge && allowed {
lastIndex := lastIndexes[relativeIndex]
formatted[r-1].cols[lastIndex].mergeBelow = true
if t.headerVerticalAlign == AlignBottom {
buffer := row.cols[c].lines
row.cols[c].lines = formatted[r-1].cols[lastIndex].lines
formatted[r-1].cols[lastIndex].lines = buffer
}
}
lastValues[relativeIndex] = current
lastIndexes[relativeIndex] = c
formatted[r] = row
}
}
Expand All @@ -675,7 +705,6 @@ func (t *Table) renderRows() {

func (t *Table) renderRow(row iRow, prev iRow) {
t.renderLineAbove(row, prev)

for y := 0; y < row.height; y++ {
if t.borders.Left {
t.setStyle(t.lineStyle)
Expand All @@ -686,7 +715,9 @@ func (t *Table) renderRow(row iRow, prev iRow) {
if t.padding > 0 {
t.print(strings.Repeat(" ", t.padding))
}
if col.mergeAbove {
if col.mergeAbove && t.headerVerticalAlign == AlignTop {
t.print(strings.Repeat(" ", col.width))
} else if col.mergeBelow && t.headerVerticalAlign == AlignBottom {
t.print(strings.Repeat(" ", col.width))
} else {
if row.header {
Expand Down
28 changes: 28 additions & 0 deletions table_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -738,3 +738,31 @@ func Test_HeaderColSpanVariation(t *testing.T) {
└─────────┴──────────┴──────────────┴────────┴─────┴─────────┴───────────────┘
`, "\n"+builder.String())
}

func Test_HeaderVerticalAlignBottom(t *testing.T) {
builder := &strings.Builder{}
table := New(builder)
table.SetHeaders("Service", "Misconfigurations", "Last Scanned")
table.AddHeaders("Service", "Critical", "High", "Medium", "Low", "Unknown", "Last Scanned")
table.SetRowLines(false)
table.SetHeaderAlignment(AlignLeft, AlignCenter, AlignCenter, AlignCenter, AlignCenter, AlignCenter, AlignLeft)
table.SetHeaderVerticalAlignment(AlignBottom)
table.SetAlignment(AlignLeft, AlignRight, AlignRight, AlignRight, AlignRight, AlignRight, AlignLeft)
table.SetAutoMergeHeaders(true)
table.SetHeaderColSpans(0, 1, 5, 1)
table.AddRow("ec2", "1", "2", "5", "0", "3", "2 hours ago")
table.AddRow("ecs", "0", "-", "-", "1", "0", "just now")
table.AddRow("eks", "7", "0", "0", "3", "0", "127 hours ago")
table.Render()
assertMultilineEqual(t, `
┌─────────┬──────────────────────────────────────────────────┬───────────────┐
│ │ Misconfigurations │ │
│ ├──────────┬──────────────┬────────┬─────┬─────────┤ │
│ Service │ Critical │ High │ Medium │ Low │ Unknown │ Last Scanned │
├─────────┼──────────┼──────────────┼────────┼─────┼─────────┼───────────────┤
│ ec2 │ 1 │ 2 │ 5 │ 0 │ 3 │ 2 hours ago │
│ ecs │ 0 │ - │ - │ 1 │ 0 │ just now │
│ eks │ 7 │ 0 │ 0 │ 3 │ 0 │ 127 hours ago │
└─────────┴──────────┴──────────────┴────────┴─────┴─────────┴───────────────┘
`, "\n"+builder.String())
}

0 comments on commit 988842c

Please sign in to comment.