-
Notifications
You must be signed in to change notification settings - Fork 6
How to display tables in the console
When building a console application it may be useful to display tabular data:
To display a table, use Table, set the headers, set the rows and then render the table:
package main
import (
"github.com/DrSmithFr/go-console"
"github.com/DrSmithFr/go-console/table"
)
func main() {
cmd := go_console.NewScript().Build()
tab := table.
NewTable().
AddHeadersFromString(
[][]string{
{"ISBN-LONG-TITLE", "Title", "Author"},
},
)
tab.
AddRowsFromString(
[][]string{
{"99921-58-10-7", "The Divine Comedy", "Dante Alighieri"},
{"9971-5-0210-0", "A Tale of Two Cities", "Charles Dickens"},
{"960-425-059-0", "The Lord of the Rings", "J. R. R. Tolkien"},
{"80-902734-1-6", "And Then There Were None", "Agatha Christie"},
},
)
render := table.
NewRender(cmd.Output).
SetContent(tab)
render.Render()
}
You can add a table separator anywhere in the output by passing an instance of TableSeparator as a row:
tab.
AddRowsFromString(
[][]string{
{"99921-58-10-7", "The Divine Comedy", "Dante Alighieri"},
{"9971-5-0210-0", "A Tale of Two Cities", "Charles Dickens"},
{"---"}, // or "==="
{"960-425-059-0", "The Lord of the Rings", "J. R. R. Tolkien"},
{"80-902734-1-6", "And Then There Were None", "Agatha Christie"},
},
)
You can optionally display titles at the top and the bottom of the table:
tab.
SetHeaderTitle("Books").
SetFooterTitle("Page 1/2")
By default, the width of the columns is calculated automatically based on their contents. Use the SetColumnWidths() method to set the column widths explicitly:
// this is equivalent to the calling SetColumnsMinWidths() and SetColumnsMaxWidths() with the same values
render.
SetColumnsWidths(map[int]int{
0: 10,
1: 0,
2: 30,
})
render.Render()
In this example, the first column width will be 10, the last column width will be 30 and the second column width will be calculated automatically because of the 0 value.
You can also set the width individually for each column with the SetColumnWidth() method. Its first argument is the column index (starting from 0) and the second argument is the column width:
render.SetColumnWidth(0, 10)
render.SetColumnWidth(2, 10)
render.Render()
The output of this command will be:
Note that you can also set the max and min width of a column individually:
render.SetColumnMaxWidth(0, 10)
render.SetColumnMinWidth(1, 15)
render.
SetColumnsMinWidths(map[int]int{
0: 10,
1: 0,
2: 30,
})
render.
SetColumnsMaxWidths(map[int]int{
0: 10,
1: 0,
2: 30,
})
render.Render()
The table style can be changed to any built-in styles via SetStyleFromName()
// same as calling nothing
render.SetStyleFromName("default")
// changes the default style to compact
render.SetStyleFromName("compact")
render.Render()
This code results in:
You can also set the style to borderless
:
// changes the default style to compact
render.SetStyleFromName("borderless")
render.Render()
You can also set the style to box
:
// changes the default style to compact
render.SetStyleFromName("box")
render.Render()
You can also set the style to box-double
:
// changes the default style to compact
render.SetStyleFromName("box-double")
render.Render()
Note:
Using shortcut "---" and "===" to insert a tableSeparator with style
box-double
will result in simple or double line separator. On every other style, it will result in a simple line separator.
If the built-in styles do not fit your need, define your own:
customStyle := table.
NewTableStyle().
SetHorizontalOutsideBorderChar("═").
SetHorizontalInsideBorderChar("─").
SetVerticalOutsideBorderChar("║").
SetVerticalInsideBorderChar("│").
SetCrossingChars("┼", "╔", "╤", "╗", "╢", "╝", "╧", "╚", "╟", "╠", "╪", "╣")
render.SetStyle(customStyle)
To make a table cell that spans multiple columns you can use a TableCell:
package main
import (
"github.com/DrSmithFr/go-console"
"github.com/DrSmithFr/go-console/table"
)
func main() {
cmd := go_console.NewScript().Build()
tab := table.
NewTable().
AddHeadersFromString(
[][]string{
{"ISBN-LONG-TITLE", "Title", "Author"},
},
)
tab.
AddRowsFromString(
[][]string{
{"99921-58-10-7", "The Divine Comedy", "Dante Alighieri"},
{"9971-5-0210-0", "A Tale of Two Cities", "Charles Dickens"},
{"---"},
{"960-425-059-0", "The Lord of the Rings", "J. R. R. Tolkien"},
{"80-902734-1-6", "And Then There Were None", "Agatha Christie"},
{"==="},
},
).
AddRow(
&table.TableRow{
Columns: map[int]table.TableColumnInterface{
0: &table.TableColumn{
Cell: &table.TableCell{
Value: "<info>This value spans use <b>3 columns</b> to get fully displayed and now to long to feet inside the table.</info>",
Colspan: 3,
PadType: table.PadToCenter,
},
},
},
},
)
render := table.
NewRender(cmd.Output).
SetContent(tab)
render.SetStyleFromName("box-double")
render.Render()
}
This results in:
Note:
You can create a title using a header cell that spans the entire table width.
You can set the padding type for each cell or column individually:
-
PadToLeft
(default) PadToCenter
PadToRight
Note:
If you set a cell padding, the column padding will be ignored.
If you set a column padding, the default padding (defined by style) will be ignored.
package main
import (
"github.com/DrSmithFr/go-console"
"github.com/DrSmithFr/go-console/table"
)
func main() {
cmd := go_console.NewScript().Build()
tab := table.
NewTable().
SetColumnPadding(3, table.PadToRight).
AddHeader(
&table.TableRow{
Columns: map[int]table.TableColumnInterface{
0: &table.TableColumn{
Cell: &table.TableCell{
Value: "Centred Header Cell",
Colspan: 3,
PadType: table.PadToCenter,
},
},
},
},
).
AddRow(
table.
NewTableRow().
AddColumn(
table.
NewTableColumn().
SetCell(
table.
NewTableCell("This value spans 2 columns.").
SetPadType(table.PadToCenter).
SetColspan(2),
),
).
AddColumn(
table.
NewTableColumn().
SetCell(
table.
NewTableCell("stand alone value"),
),
),
).
AddTableSeparator().
AddRowsFromString(
[][]string{
{"960-425-059-0", "The Lord of the Rings", "J. R. R. Tolkien"},
{"80-902734-1-6", "And Then There Were None", "Agatha Christie"},
},
)
render := table.
NewRender(cmd.Output).
SetContent(tab)
render.SetColumnMinWidth(2, 13)
render.SetStyleFromName("box-double")
render.Render()
}
You can generate a table directly form a struct or a list:
Note:
Parsing List or Slice will behave the same as parsing a single struct. The only difference is that rows will be generated for each item in the list.
ParsingConfig is optional and behaves the same for both structs and lists.
Pointer fields will be dereferenced and their value will be used instead.
ackage main
import (
"github.com/DrSmithFr/go-console"
"github.com/DrSmithFr/go-console/table"
)
func main() {
type Address struct {
City string
Country string
}
type Author struct {
Name string
age int
Address *Address `display:"inline"`
}
type Book struct {
ISBN *string `header:"ID"`
Title string
Secret string `display:"hidden"`
Author *Author
}
ptrStr := func(str string) *string {
return &str
}
book := Book{
ISBN: ptrStr("99921-58-10-7"),
Title: "The Divine Comedy",
Secret: "This is a secret!",
Author: &Author{
Name: "Dante Alighieri",
age: 56,
Address: &Address{
City: "Florence",
Country: "Italy",
},
},
}
cmd := go_console.NewScript().Build()
tab := table.
NewTable().
// Helpers to set the parser config.
SetParseTagsFieldsOnly(false).
SetParseUnexportedFields(false).
SetParseMaxDepth(1).
// Or set the parser config directly.
SetParseConfig(table.ParserConfig{
TagsFieldsOnly: false,
UnexportedFields: false,
MaxDepth: 1,
})
tab.Parse(book)
render := table.
NewRender(cmd.Output).
SetContent(tab)
render.Render()
}
This results in:
With the display
tag, you can set the display mode for each field:
-
hidden
(to hide the field value and header) -
inline
(to inline value, see MaxDepth for more details)
You can set the header name for each field using the header
tag, and depending on the field type, you can also set some options use to compile value for the given field.
type Book struct {
ISBN string `header:"ID"`
Title string `header:"Book Title"`
PrintAt int64 `header:"Published,timestamp(ms|utc|RFC850)"`
Readers []string `header:"Readers,count"`
}
This results in:
const (
// Timestamp unit
TimestampFromMillisecondsHeaderTag = "ms"
// Timezone
TimestampAsUTCHeaderTag = "utc"
TimestampAsLocalHeaderTag = "local"
// Formats
TimestampFormatHumanHeaderTag = "human"
TimestampFormatANSICHeaderTag = "ANSIC"
TimestampFormatUnixDateCHeaderTag = "UnixDate"
TimestampFormatRubyDateHeaderTag = "RubyDate"
TimestampFormatRFC822HeaderTag = "RFC822"
TimestampFormatRFC822ZHeaderTag = "RFC822Z"
TimestampFormatRFC850HeaderTag = "RFC850"
TimestampFormatRFC1123HeaderTag = "RFC1123"
TimestampFormatRFC1123ZHeaderTag = "RFC1123Z" // default one.
TimestampFormatRFC3339HeaderTag = "RFC3339"
TimestampFormatARFC3339NanoHeaderTag = "RFC3339Nano"
)
Using the same example as above, we can change the parsing config to get different results:
SetParseConfig(table.ParserConfig{
TagsFieldsOnly: true,
UnexportedFields: false,
MaxDepth: 1,
})
Because Book.ISBN is the only field that implement a header
tag, it will result in:
Using the same example as above, we can change the UnexportedFields config to get different results:
SetParseConfig(table.ParserConfig{
TagsFieldsOnly: false,
UnexportedFields: true,
MaxDepth: 1,
})
Now Author.age will also be included in the result:
Using the same example as above, we can change the MaxDepth config to get different results:
Note:
MaxDepth is used to limit the depth of the parsing. It is useful to avoid infinite recursion when parsing a struct that contains itself.
When MaxDepth is set rematch, the parser will display
fmt.Sprintf("%v", field)
.
SetParseConfig(table.ParserConfig{
TagsFieldsOnly: false,
UnexportedFields: false,
MaxDepth: 0,
})
Now Author became to deep will be displayed as &{Dante Alighieri 56 0xc0000b4000}
:
Note:
If you use
display:"inline"
tag, the parser will count them as the same depth level as the parent struct. (even with multiple levels of inline structs)
For example, if we change the Author struct to:
type Address struct {
City string
Country string
}
type Author struct {
Name string
age int
Address *Address `display:"inline"`
}
type Book struct {
ISBN *string `header:"ID"`
Title string
Secret string `display:"hidden"`
Author *Author `display:"inline"`
}
then the result with MaxDepth set to 0 will be:
You can generate a table directly form a JSON formatted []Byte:
package main
import (
"encoding/json"
"github.com/DrSmithFr/go-console"
"github.com/DrSmithFr/go-console/table"
)
func main() {
cmd := go_console.NewScript().Build()
jsonData := getMyJSONBytes()
tab := table.
NewTable().
ParseJSON(jsonData)
render := table.
NewRender(cmd.Output).
SetContent(tab)
render.Render()
}
func getMyJSONBytes() []byte {
data := struct {
// json tags are optionally but if set they are being used for the headers on `PrintJSON`.
Firstname string `json:"first name"`
Lastname string `json:"last name"`
}{"Georgios", "Callas"}
b, err := json.MarshalIndent(data, "", " ")
if err != nil {
panic(err)
}
return b
}
Internally the JSON is Unmarshalled and can get parsed as a struct. So you can use the same parsing configuration as for the Parsing Struct and List to manage the display.
You can generate a table directly form a map[string]interface{}:
Note:
Keys are used as headers.
Values are used as rows and must implement
fmt.Stringer
interface.
package main
import (
"github.com/DrSmithFr/go-console"
"github.com/DrSmithFr/go-console/table"
)
type Book struct {
ISBN string
Title string
}
func (b Book) String() string {
return b.ISBN
}
func main() {
books := map[string][]Book{
"bookshelves 1": {
{ISBN: "99921-58-10-7", Title: "The Divine Comedy"},
{ISBN: "9971-5-0210-0", Title: "A Tale of Two Cities"},
},
"bookshelves 2": {
{ISBN: "960-425-059-0", Title: "The Lord of the Rings"},
{ISBN: "80-902734-1-6", Title: "And Then There Were None"},
},
}
cmd := go_console.NewScript().Build()
tab := table.
NewTable().
Parse(books)
render := table.
NewRender(cmd.Output).
SetContent(tab)
render.Render()
}
Will result in: