diff --git a/cmd/demo-table/demo.go b/cmd/demo-table/demo.go index 4aebea1..a79908d 100644 --- a/cmd/demo-table/demo.go +++ b/cmd/demo-table/demo.go @@ -9,15 +9,23 @@ import ( "github.com/jedib0t/go-pretty/v6/text" ) +var ( + colTitleIndex = "#" + colTitleFirstName = "First Name" + colTitleLastName = "Last Name" + colTitleSalary = "Salary" + rowHeader = table.Row{colTitleIndex, colTitleFirstName, colTitleLastName, colTitleSalary} + row1 = table.Row{1, "Arya", "Stark", 3000} + row2 = table.Row{20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"} + row3 = table.Row{300, "Tyrion", "Lannister", 5000} + rowFooter = table.Row{"", "", "Total", 10000} +) + func demoTableColors() { tw := table.NewWriter() - tw.AppendHeader(table.Row{"#", "First Name", "Last Name", "Salary"}) - tw.AppendRows([]table.Row{ - {1, "Arya", "Stark", 3000}, - {20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"}, - {300, "Tyrion", "Lannister", 5000}, - }) - tw.AppendFooter(table.Row{"", "", "Total", 10000}) + tw.AppendHeader(rowHeader) + tw.AppendRows([]table.Row{row1, row2, row3}) + tw.AppendFooter(rowFooter) tw.SetIndexColumn(1) tw.SetTitle("Game Of Thrones") @@ -99,7 +107,7 @@ func demoTableFeatures() { //+---+-----+--------+-----------+------+-----------------------------+ //Table with Auto-Indexing. // - t.AppendHeader(table.Row{"#", "First Name", "Last Name", "Salary"}) + t.AppendHeader(rowHeader) t.SetCaption("Table with Auto-Indexing (columns-only).\n") fmt.Println(t.Render()) //+---+-----+------------+-----------+--------+-----------------------------+ @@ -151,7 +159,7 @@ func demoTableFeatures() { // go right and everything else left. but what if you want the first name to // go right too? and the last column to be "justified"? t.SetColumnConfigs([]table.ColumnConfig{ - {Name: "First Name", Align: text.AlignRight}, + {Name: colTitleFirstName, Align: text.AlignRight}, // the 5th column does not have a title, so use the column number as the // identifier for the column {Number: 5, Align: text.AlignJustify}, @@ -202,9 +210,9 @@ func demoTableFeatures() { // // time to Align/VAlign the columns... t.SetColumnConfigs([]table.ColumnConfig{ - {Name: "First Name", Align: text.AlignRight, VAlign: text.VAlignMiddle}, - {Name: "Last Name", VAlign: text.VAlignBottom}, - {Name: "Salary", Align: text.AlignRight, VAlign: text.VAlignMiddle}, + {Name: colTitleFirstName, Align: text.AlignRight, VAlign: text.VAlignMiddle}, + {Name: colTitleLastName, VAlign: text.VAlignBottom}, + {Name: colTitleSalary, Align: text.AlignRight, VAlign: text.VAlignMiddle}, // the 5th column does not have a title, so use the column number {Number: 5, Align: text.AlignJustify}, }) @@ -229,9 +237,9 @@ func demoTableFeatures() { // // changed your mind about AlignJustify? t.SetColumnConfigs([]table.ColumnConfig{ - {Name: "First Name", Align: text.AlignRight, VAlign: text.VAlignMiddle}, - {Name: "Last Name", VAlign: text.VAlignBottom}, - {Name: "Salary", Align: text.AlignRight, VAlign: text.VAlignMiddle}, + {Name: colTitleFirstName, Align: text.AlignRight, VAlign: text.VAlignMiddle}, + {Name: colTitleLastName, VAlign: text.VAlignBottom}, + {Name: colTitleSalary, Align: text.AlignRight, VAlign: text.VAlignMiddle}, {Number: 5, Align: text.AlignCenter}, }) t.SetCaption("Table with a Multi-line Row with VAlign and changed Align.\n") @@ -259,10 +267,10 @@ func demoTableFeatures() { // custom separators? //========================================================================== t.ResetRows() - t.AppendRow(table.Row{1, "Arya", "Stark", 3000}) - t.AppendRow(table.Row{20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"}) + t.AppendRow(row1) + t.AppendRow(row2) t.AppendSeparator() - t.AppendRow([]interface{}{300, "Tyrion", "Lannister", 5000}) + t.AppendRow(row3) t.SetCaption("Simple Table with 3 Rows and a Separator in-between.\n") fmt.Println(t.Render()) //+-----+--------+-----------+------+-----------------------------+ @@ -279,9 +287,9 @@ func demoTableFeatures() { //========================================================================== t.ResetRows() t.SetColumnConfigs(nil) - t.AppendRow(table.Row{1, "Arya", "Stark", 3000}) - t.AppendRow(table.Row{20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"}) - t.AppendRow([]interface{}{300, "Tyrion", "Lannister", 5000}) + t.AppendRow(row1) + t.AppendRow(row2) + t.AppendRow(row3) t.SetCaption("Starting afresh with a Simple Table again.\n") fmt.Println(t.Render()) //+-----+------------+-----------+--------+-----------------------------+ @@ -364,9 +372,9 @@ func demoTableFeatures() { // But I want to see all the data! //========================================================================== t.SetColumnConfigs([]table.ColumnConfig{ - {Name: "First Name", WidthMax: 6}, - {Name: "Last Name", WidthMax: 9}, - {Name: "Salary", WidthMax: 6}, + {Name: colTitleFirstName, WidthMax: 6}, + {Name: colTitleLastName, WidthMax: 9}, + {Name: colTitleSalary, WidthMax: 6}, {Number: 5, WidthMax: 10}, }) t.SetCaption("Table on a diet.\n") @@ -475,10 +483,10 @@ func demoTableFeatures() { colorBOnW := text.Colors{text.BgWhite, text.FgBlack} // set colors using Colors/ColorsHeader/ColorsFooter t.SetColumnConfigs([]table.ColumnConfig{ - {Name: "#", Colors: text.Colors{text.FgYellow}, ColorsHeader: colorBOnW}, - {Name: "First Name", Colors: text.Colors{text.FgHiRed}, ColorsHeader: colorBOnW}, - {Name: "Last Name", Colors: text.Colors{text.FgHiRed}, ColorsHeader: colorBOnW, ColorsFooter: colorBOnW}, - {Name: "Salary", Colors: text.Colors{text.FgGreen}, ColorsHeader: colorBOnW, ColorsFooter: colorBOnW}, + {Name: colTitleIndex, Colors: text.Colors{text.FgYellow}, ColorsHeader: colorBOnW}, + {Name: colTitleFirstName, Colors: text.Colors{text.FgHiRed}, ColorsHeader: colorBOnW}, + {Name: colTitleLastName, Colors: text.Colors{text.FgHiRed}, ColorsHeader: colorBOnW, ColorsFooter: colorBOnW}, + {Name: colTitleSalary, Colors: text.Colors{text.FgGreen}, ColorsHeader: colorBOnW, ColorsFooter: colorBOnW}, {Number: 5, Colors: text.Colors{text.FgCyan}, ColorsHeader: colorBOnW}, }) t.SetCaption("Table with Colors.\n") diff --git a/list/render.go b/list/render.go index 54d4388..5e9bce7 100644 --- a/list/render.go +++ b/list/render.go @@ -57,14 +57,14 @@ func (l *List) renderItem(out *strings.Builder, idx int, item *listItem, hint re // render the prefix or the leading text before the actual item l.renderItemBulletPrefix(out, idx, item.Level, lineIdx, hint) - l.renderItemBullet(out, idx, item.Level, lineIdx, hint) + l.renderItemBullet(out, lineIdx, hint) // render the actual item out.WriteString(lineStr) } } -func (l *List) renderItemBullet(out *strings.Builder, itemIdx int, itemLevel int, lineIdx int, hint renderHint) { +func (l *List) renderItemBullet(out *strings.Builder, lineIdx int, hint renderHint) { if lineIdx > 0 { // multi-line item.Text if hint.isLastItem { @@ -73,24 +73,28 @@ func (l *List) renderItemBullet(out *strings.Builder, itemIdx int, itemLevel int out.WriteString(l.style.CharItemVertical) } } else { - // single-line item.Text (or first line of a multi-line item.Text) - if hint.isOnlyItem { - if hint.isTopItem { - out.WriteString(l.style.CharItemSingle) - } else { - out.WriteString(l.style.CharItemBottom) - } - } else if hint.isTopItem { - out.WriteString(l.style.CharItemTop) - } else if hint.isFirstItem { - out.WriteString(l.style.CharItemFirst) - } else if hint.isBottomItem || hint.isLastItem { - out.WriteString(l.style.CharItemBottom) + l.renderItemBulletSingleLine(out, lineIdx, hint) + } +} + +func (l *List) renderItemBulletSingleLine(out *strings.Builder, lineIdx int, hint renderHint) { + // single-line item.Text (or first line of a multi-line item.Text) + if hint.isOnlyItem { + if hint.isTopItem { + out.WriteString(l.style.CharItemSingle) } else { - out.WriteString(l.style.CharItemMiddle) + out.WriteString(l.style.CharItemBottom) } - out.WriteRune(' ') + } else if hint.isTopItem { + out.WriteString(l.style.CharItemTop) + } else if hint.isFirstItem { + out.WriteString(l.style.CharItemFirst) + } else if hint.isBottomItem || hint.isLastItem { + out.WriteString(l.style.CharItemBottom) + } else { + out.WriteString(l.style.CharItemMiddle) } + out.WriteRune(' ') } func (l *List) renderItemBulletPrefix(out *strings.Builder, itemIdx int, itemLevel int, lineIdx int, hint renderHint) { diff --git a/progress/render.go b/progress/render.go index 80d6f2c..a778622 100644 --- a/progress/render.go +++ b/progress/render.go @@ -318,23 +318,7 @@ func (p *Progress) renderTrackerStats(out *strings.Builder, t *Tracker, hint ren outStats.WriteString(" in ") } if !hint.hideTime { - var td, tp time.Duration - if t.IsDone() { - td = t.timeStop.Sub(t.timeStart) - } else { - td = time.Since(t.timeStart) - } - if hint.isOverallTracker { - tp = p.style.Options.TimeOverallPrecision - } else if t.IsDone() { - tp = p.style.Options.TimeDonePrecision - } else { - tp = p.style.Options.TimeInProgressPrecision - } - outStats.WriteString(p.style.Colors.Time.Sprint(td.Round(tp))) - if p.showETA || hint.isOverallTracker { - p.renderTrackerStatsETA(&outStats, t, hint) - } + p.renderTrackerStatsTime(&outStats, t, hint) } outStats.WriteRune(']') @@ -342,6 +326,26 @@ func (p *Progress) renderTrackerStats(out *strings.Builder, t *Tracker, hint ren } } +func (p *Progress) renderTrackerStatsTime(outStats *strings.Builder, t *Tracker, hint renderHint) { + var td, tp time.Duration + if t.IsDone() { + td = t.timeStop.Sub(t.timeStart) + } else { + td = time.Since(t.timeStart) + } + if hint.isOverallTracker { + tp = p.style.Options.TimeOverallPrecision + } else if t.IsDone() { + tp = p.style.Options.TimeDonePrecision + } else { + tp = p.style.Options.TimeInProgressPrecision + } + outStats.WriteString(p.style.Colors.Time.Sprint(td.Round(tp))) + if p.showETA || hint.isOverallTracker { + p.renderTrackerStatsETA(outStats, t, hint) + } +} + func (p *Progress) renderTrackerStatsETA(out *strings.Builder, t *Tracker, hint renderHint) { tpETA := p.style.Options.ETAPrecision if eta := t.ETA().Round(tpETA); hint.isOverallTracker || eta > tpETA { diff --git a/progress/units.go b/progress/units.go index 96c93dd..5839fac 100644 --- a/progress/units.go +++ b/progress/units.go @@ -4,7 +4,7 @@ import ( "fmt" ) -// UnitsNotationPosition determines units position relative of tracker value. +// UnitsNotationPosition determines notation position relative to unit value. type UnitsNotationPosition int // Supported unit positions relative to tracker value; @@ -16,68 +16,72 @@ const ( // Units defines the "type" of the value being tracked by the Tracker. type Units struct { + Formatter func(value int64) string // default: FormatNumber Notation string - NotationPosition UnitsNotationPosition - Formatter func(value int64) string + NotationPosition UnitsNotationPosition // default: UnitsNotationPositionBefore +} + +// Sprint prints the value as defined by the Units. +func (tu Units) Sprint(value int64) string { + formatter := tu.Formatter + if formatter == nil { + formatter = FormatNumber + } + + formattedValue := formatter(value) + switch tu.NotationPosition { + case UnitsNotationPositionAfter: + return formattedValue + tu.Notation + default: // UnitsNotationPositionBefore + return tu.Notation + formattedValue + } } var ( // UnitsDefault doesn't define any units. The value will be treated as any // other number. UnitsDefault = Units{ - Notation: "", - Formatter: FormatNumber, + Notation: "", + NotationPosition: UnitsNotationPositionBefore, + Formatter: FormatNumber, } // UnitsBytes defines the value as a storage unit. Values will be converted // and printed in one of these forms: B, KB, MB, GB, TB, PB UnitsBytes = Units{ - Notation: "", - Formatter: FormatBytes, + Notation: "", + NotationPosition: UnitsNotationPositionBefore, + Formatter: FormatBytes, } // UnitsCurrencyDollar defines the value as a Dollar amount. Values will be // converted and printed in one of these forms: $x.yz, $x.yzK, $x.yzM, // $x.yzB, $x.yzT UnitsCurrencyDollar = Units{ - Notation: "$", - Formatter: FormatNumber, + Notation: "$", + NotationPosition: UnitsNotationPositionBefore, + Formatter: FormatNumber, } // UnitsCurrencyEuro defines the value as a Euro amount. Values will be // converted and printed in one of these forms: ₠x.yz, ₠x.yzK, ₠x.yzM, // ₠x.yzB, ₠x.yzT UnitsCurrencyEuro = Units{ - Notation: "₠", - Formatter: FormatNumber, + Notation: "₠", + NotationPosition: UnitsNotationPositionBefore, + Formatter: FormatNumber, } // UnitsCurrencyPound defines the value as a Pound amount. Values will be // converted and printed in one of these forms: £x.yz, £x.yzK, £x.yzM, // £x.yzB, £x.yzT UnitsCurrencyPound = Units{ - Notation: "£", - Formatter: FormatNumber, + Notation: "£", + NotationPosition: UnitsNotationPositionBefore, + Formatter: FormatNumber, } ) -// Sprint prints the value as defined by the Units. -func (tu Units) Sprint(value int64) string { - formatter := tu.Formatter - if formatter == nil { - formatter = FormatNumber - } - - formattedValue := formatter(value) - - switch tu.NotationPosition { - case UnitsNotationPositionAfter: - return formattedValue + tu.Notation - default: // UnitsNotationPositionBefore - return tu.Notation + formattedValue - } -} - // FormatBytes formats the given value as a "Byte". func FormatBytes(value int64) string { if value < 1000 { diff --git a/table/render.go b/table/render.go index 2fa1772..0537115 100644 --- a/table/render.go +++ b/table/render.go @@ -180,7 +180,7 @@ func (t *Table) renderLine(out *strings.Builder, row rowStr, hint renderHint) { out.WriteRune('\n') } - // use a brand new strings.Builder if a row length limit has been set + // use a brand-new strings.Builder if a row length limit has been set var outLine *strings.Builder if t.allowedRowLength > 0 { outLine = &strings.Builder{} @@ -202,16 +202,7 @@ func (t *Table) renderLine(out *strings.Builder, row rowStr, hint renderHint) { // merge the strings.Builder objects if a new one was created earlier if outLine != out { - outLineStr := outLine.String() - if text.RuneCount(outLineStr) > t.allowedRowLength { - trimLength := t.allowedRowLength - utf8.RuneCountInString(t.style.Box.UnfinishedRow) - if trimLength > 0 { - out.WriteString(text.Trim(outLineStr, trimLength)) - out.WriteString(t.style.Box.UnfinishedRow) - } - } else { - out.WriteString(outLineStr) - } + t.renderLineMergeOutputs(out, outLine) } // if a page size has been set, and said number of lines has already @@ -229,27 +220,22 @@ func (t *Table) renderLine(out *strings.Builder, row rowStr, hint renderHint) { } } -func (t *Table) renderMarginLeft(out *strings.Builder, hint renderHint) { - if t.style.Options.DrawBorder { - border := t.style.Box.Left - if hint.isBorderTop { - if t.title != "" { - border = t.style.Box.LeftSeparator - } else { - border = t.style.Box.TopLeft - } - } else if hint.isBorderBottom { - border = t.style.Box.BottomLeft - } else if hint.isSeparatorRow { - if t.autoIndex && hint.isHeaderOrFooterSeparator() { - border = t.style.Box.Left - } else if !t.autoIndex && t.shouldMergeCellsVertically(0, hint) { - border = t.style.Box.Left - } else { - border = t.style.Box.LeftSeparator - } +func (t *Table) renderLineMergeOutputs(out *strings.Builder, outLine *strings.Builder) { + outLineStr := outLine.String() + if text.RuneCount(outLineStr) > t.allowedRowLength { + trimLength := t.allowedRowLength - utf8.RuneCountInString(t.style.Box.UnfinishedRow) + if trimLength > 0 { + out.WriteString(text.Trim(outLineStr, trimLength)) + out.WriteString(t.style.Box.UnfinishedRow) } + } else { + out.WriteString(outLineStr) + } +} +func (t *Table) renderMarginLeft(out *strings.Builder, hint renderHint) { + if t.style.Options.DrawBorder { + border := t.getBorderLeft(hint) colors := t.getBorderColors(hint) if colors.EscapeSeq() != "" { out.WriteString(colors.Sprint(border)) @@ -261,23 +247,7 @@ func (t *Table) renderMarginLeft(out *strings.Builder, hint renderHint) { func (t *Table) renderMarginRight(out *strings.Builder, hint renderHint) { if t.style.Options.DrawBorder { - border := t.style.Box.Right - if hint.isBorderTop { - if t.title != "" { - border = t.style.Box.RightSeparator - } else { - border = t.style.Box.TopRight - } - } else if hint.isBorderBottom { - border = t.style.Box.BottomRight - } else if hint.isSeparatorRow { - if t.shouldMergeCellsVertically(t.numColumns-1, hint) { - border = t.style.Box.Right - } else { - border = t.style.Box.RightSeparator - } - } - + border := t.getBorderRight(hint) colors := t.getBorderColors(hint) if colors.EscapeSeq() != "" { out.WriteString(colors.Sprint(border)) @@ -292,16 +262,7 @@ func (t *Table) renderRow(out *strings.Builder, row rowStr, hint renderHint) { // fit every column into the allowedColumnLength/maxColumnLength limit // and in the process find the max. number of lines in any column in // this row - colMaxLines := 0 - rowWrapped := make(rowStr, len(row)) - for colIdx, colStr := range row { - widthEnforcer := t.columnConfigMap[colIdx].getWidthMaxEnforcer() - rowWrapped[colIdx] = widthEnforcer(colStr, t.maxColumnLengths[colIdx]) - colNumLines := strings.Count(rowWrapped[colIdx], "\n") + 1 - if colNumLines > colMaxLines { - colMaxLines = colNumLines - } - } + colMaxLines, rowWrapped := t.wrapRow(row) // if there is just 1 line in all columns, add the row as such; else // split each column into individual lines and render them one-by-one @@ -410,22 +371,26 @@ func (t *Table) renderTitle(out *strings.Builder) { } titleText := text.WrapText(t.title, lenText) for _, titleLine := range strings.Split(titleText, "\n") { - titleLine = strings.TrimSpace(titleLine) - titleLine = t.style.Title.Format.Apply(titleLine) - titleLine = t.style.Title.Align.Apply(titleLine, lenText) - titleLine = t.style.Box.PaddingLeft + titleLine + t.style.Box.PaddingRight - titleLine = t.style.Title.Colors.Sprint(titleLine) - - if out.Len() > 0 { - out.WriteRune('\n') - } - if t.style.Options.DrawBorder { - out.WriteString(t.style.Box.Left) - } - out.WriteString(titleLine) - if t.style.Options.DrawBorder { - out.WriteString(t.style.Box.Right) - } + t.renderTitleLine(out, lenText, titleLine) } } } + +func (t *Table) renderTitleLine(out *strings.Builder, lenText int, titleLine string) { + titleLine = strings.TrimSpace(titleLine) + titleLine = t.style.Title.Format.Apply(titleLine) + titleLine = t.style.Title.Align.Apply(titleLine, lenText) + titleLine = t.style.Box.PaddingLeft + titleLine + t.style.Box.PaddingRight + titleLine = t.style.Title.Colors.Sprint(titleLine) + + if out.Len() > 0 { + out.WriteRune('\n') + } + if t.style.Options.DrawBorder { + out.WriteString(t.style.Box.Left) + } + out.WriteString(titleLine) + if t.style.Options.DrawBorder { + out.WriteString(t.style.Box.Right) + } +} diff --git a/table/render_html.go b/table/render_html.go index 3f550dd..10a7325 100644 --- a/table/render_html.go +++ b/table/render_html.go @@ -78,6 +78,22 @@ func (t *Table) RenderHTML() string { return t.render(&out) } +func (t *Table) htmlGetColStrAndTag(row rowStr, colIdx int, hint renderHint) (string, string) { + // get the column contents + var colStr string + if colIdx < len(row) { + colStr = row[colIdx] + } + + // header uses "th" instead of "td" + colTagName := "td" + if hint.isHeaderRow { + colTagName = "th" + } + + return colStr, colTagName +} + func (t *Table) htmlRenderCaption(out *strings.Builder) { if t.caption != "" { out.WriteString("