mirror of
https://github.com/owncloud/ocis
synced 2026-04-25 17:25:21 +02:00
* chore(deps): bump github.com/olekukonko/tablewriter from 0.0.5 to 1.0.9 Bumps [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) from 0.0.5 to 1.0.9. - [Commits](https://github.com/olekukonko/tablewriter/compare/v0.0.5...v1.0.9) --- updated-dependencies: - dependency-name: github.com/olekukonko/tablewriter dependency-version: 1.0.9 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> * fix: adjust to tablewriter 1.0.9 (AI-powered) Signed-off-by: Julian Koberg <julian.koberg@kiteworks.com> * update to NewTable * bring back a Footer * fix the tests --------- Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: Julian Koberg <julian.koberg@kiteworks.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Julian Koberg <julian.koberg@kiteworks.com> Co-authored-by: Roman Perekhod <rperekhod@owncloud.com>
1101 lines
34 KiB
Markdown
1101 lines
34 KiB
Markdown
# TableWriter for Go
|
|
|
|
[](https://github.com/olekukonko/tablewriter/actions/workflows/go.yml)
|
|
[](https://pkg.go.dev/github.com/olekukonko/tablewriter)
|
|
[](https://goreportcard.com/report/github.com/olekukonko/tablewriter)
|
|
[](LICENSE)
|
|
[](README.md#benchmarks)
|
|
|
|
`tablewriter` is a Go library for generating **rich text-based tables** with support for multiple output formats, including ASCII, Unicode, Markdown, HTML, and colorized terminals. Perfect for CLI tools, logs, and web applications.
|
|
|
|
### Key Features
|
|
- **Multi-format rendering**: ASCII, Unicode, Markdown, HTML, ANSI-colored
|
|
- **Advanced styling**: Cell merging, alignment, padding, borders
|
|
- **Flexible input**: CSV, structs, slices, or streaming data
|
|
- **High performance**: Minimal allocations, buffer reuse
|
|
- **Modern features**: Generics support, hierarchical merging, real-time streaming
|
|
|
|
---
|
|
|
|
### Installation
|
|
|
|
#### Legacy Version (v0.0.5)
|
|
For use with legacy applications:
|
|
```bash
|
|
go get github.com/olekukonko/tablewriter@v0.0.5
|
|
```
|
|
|
|
#### Latest Version
|
|
The latest stable version
|
|
```bash
|
|
go get github.com/olekukonko/tablewriter@v1.0.9
|
|
```
|
|
|
|
**Warning:** Version `v1.0.0` contains missing functionality and should not be used.
|
|
|
|
|
|
> **Version Guidance**
|
|
> - Legacy: Use `v0.0.5` (stable)
|
|
> - New Features: Use `@latest` (includes generics, super fast streaming APIs)
|
|
> - Legacy Docs: See [README_LEGACY.md](README_LEGACY.md)
|
|
|
|
---
|
|
|
|
### Why TableWriter?
|
|
- **CLI Ready**: Instant compatibility with terminal outputs
|
|
- **Database Friendly**: Native support for `sql.Null*` types
|
|
- **Secure**: Auto-escaping for HTML/Markdown
|
|
- **Extensible**: Custom renderers and formatters
|
|
|
|
---
|
|
|
|
### Quick Example
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"github.com/olekukonko/tablewriter"
|
|
"os"
|
|
)
|
|
|
|
func main() {
|
|
data := [][]string{
|
|
{"Package", "Version", "Status"},
|
|
{"tablewriter", "v0.0.5", "legacy"},
|
|
{"tablewriter", "v1.0.9", "latest"},
|
|
}
|
|
|
|
table := tablewriter.NewWriter(os.Stdout)
|
|
table.Header(data[0])
|
|
table.Bulk(data[1:])
|
|
table.Render()
|
|
}
|
|
```
|
|
**Output**:
|
|
```
|
|
┌─────────────┬─────────┬────────┐
|
|
│ PACKAGE │ VERSION │ STATUS │
|
|
├─────────────┼─────────┼────────┤
|
|
│ tablewriter │ v0.0.5 │ legacy │
|
|
│ tablewriter │ v1.0.9 │ latest │
|
|
└─────────────┴─────────┴────────┘
|
|
```
|
|
|
|
|
|
## Detailed Usage
|
|
|
|
Create a table with `NewTable` or `NewWriter`, configure it using options or a `Config` struct, add data with `Append` or `Bulk`, and render to an `io.Writer`. Use renderers like `Blueprint` (ASCII), `HTML`, `Markdown`, `Colorized`, or `Ocean` (streaming).
|
|
|
|
Here's how the API primitives map to the generated ASCII table:
|
|
|
|
```
|
|
API Call ASCII Table Component
|
|
-------- ---------------------
|
|
|
|
table.Header([]string{"NAME", "AGE"}) ┌──────┬─────┐ ← Borders.Top
|
|
│ NAME │ AGE │ ← Header row
|
|
├──────┼─────┤ ← Lines.ShowTop (header separator)
|
|
|
|
table.Append([]string{"Alice", "25"}) │ Alice│ 25 │ ← Data row
|
|
├──────┼─────┤ ← Separators.BetweenRows
|
|
|
|
table.Append([]string{"Bob", "30"}) │ Bob │ 30 │ ← Data row
|
|
├──────┼─────┤ ← Lines.ShowBottom (footer separator)
|
|
|
|
table.Footer([]string{"Total", "2"}) │ Total│ 2 │ ← Footer row
|
|
└──────┴─────┘ ← Borders.Bottom
|
|
```
|
|
|
|
The core components include:
|
|
|
|
- **Renderer** - Implements the core interface for converting table data into output formats. Available renderers include Blueprint (ASCII), HTML, Markdown, Colorized (ASCII with color), Ocean (streaming ASCII), and SVG.
|
|
|
|
- **Config** - The root configuration struct that controls all table behavior and appearance
|
|
- **Behavior** - Controls high-level rendering behaviors including auto-hiding empty columns, trimming row whitespace, header/footer visibility, and compact mode for optimized merged cell calculations
|
|
- **CellConfig** - The comprehensive configuration template used for table sections (header, row, footer). Combines formatting, padding, alignment, filtering, callbacks, and width constraints with global and per-column control
|
|
- **StreamConfig** - Configuration for streaming mode including enable/disable state and strict column validation
|
|
|
|
- **Rendition** - Defines how a renderer formats tables and contains the complete visual styling configuration
|
|
- **Borders** - Control the outer frame visibility (top, bottom, left, right edges) of the table
|
|
- **Lines** - Control horizontal boundary lines (above/below headers, above footers) that separate different table sections
|
|
- **Separators** - Control the visibility of separators between rows and between columns within the table content
|
|
- **Symbols** - Define the characters used for drawing table borders, corners, and junctions
|
|
|
|
These components can be configured with various `tablewriter.With*()` functional options when creating a new table.
|
|
|
|
## Examples
|
|
|
|
### Basic Examples
|
|
|
|
#### 1. Simple Tables
|
|
|
|
Create a basic table with headers and rows.
|
|
|
|
|
|
##### default
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/olekukonko/tablewriter"
|
|
"os"
|
|
)
|
|
|
|
type Age int
|
|
|
|
func (a Age) String() string {
|
|
return fmt.Sprintf("%d yrs", a)
|
|
}
|
|
|
|
func main() {
|
|
data := [][]any{
|
|
{"Alice", Age(25), "New York"},
|
|
{"Bob", Age(30), "Boston"},
|
|
}
|
|
|
|
table := tablewriter.NewTable(os.Stdout)
|
|
table.Header("Name", "Age", "City")
|
|
table.Bulk(data)
|
|
table.Render()
|
|
}
|
|
```
|
|
|
|
**Output**:
|
|
|
|
```
|
|
┌───────┬────────┬──────────┐
|
|
│ NAME │ AGE │ CITY │
|
|
├───────┼────────┼──────────┤
|
|
│ Alice │ 25 yrs │ New York │
|
|
│ Bob │ 30 yrs │ Boston │
|
|
└───────┴────────┴──────────┘
|
|
|
|
```
|
|
|
|
|
|
##### with customization
|
|
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/olekukonko/tablewriter"
|
|
"github.com/olekukonko/tablewriter/renderer"
|
|
"github.com/olekukonko/tablewriter/tw"
|
|
"os"
|
|
)
|
|
|
|
type Age int
|
|
|
|
func (a Age) String() string {
|
|
return fmt.Sprintf("%d yrs", a)
|
|
}
|
|
|
|
func main() {
|
|
data := [][]any{
|
|
{"Alice", Age(25), "New York"},
|
|
{"Bob", Age(30), "Boston"},
|
|
}
|
|
|
|
symbols := tw.NewSymbolCustom("Nature").
|
|
WithRow("~").
|
|
WithColumn("|").
|
|
WithTopLeft("🌱").
|
|
WithTopMid("🌿").
|
|
WithTopRight("🌱").
|
|
WithMidLeft("🍃").
|
|
WithCenter("❀").
|
|
WithMidRight("🍃").
|
|
WithBottomLeft("🌻").
|
|
WithBottomMid("🌾").
|
|
WithBottomRight("🌻")
|
|
|
|
table := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{Symbols: symbols})))
|
|
table.Header("Name", "Age", "City")
|
|
table.Bulk(data)
|
|
table.Render()
|
|
}
|
|
```
|
|
|
|
```
|
|
🌱~~~~~~❀~~~~~~~~❀~~~~~~~~~🌱
|
|
| NAME | AGE | CITY |
|
|
🍃~~~~~~❀~~~~~~~~❀~~~~~~~~~🍃
|
|
| Alice | 25 yrs | New York |
|
|
| Bob | 30 yrs | Boston |
|
|
🌻~~~~~~❀~~~~~~~~❀~~~~~~~~~🌻
|
|
```
|
|
|
|
See [symbols example](https://github.com/olekukonko/tablewriter/blob/master/_example/symbols/main.go) for more
|
|
|
|
#### 2. Markdown Table
|
|
|
|
Generate a Markdown table for documentation.
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/olekukonko/tablewriter"
|
|
"github.com/olekukonko/tablewriter/renderer"
|
|
"os"
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
type Name struct {
|
|
First string
|
|
Last string
|
|
}
|
|
|
|
// this will be ignored since Format() is present
|
|
func (n Name) String() string {
|
|
return fmt.Sprintf("%s %s", n.First, n.Last)
|
|
}
|
|
|
|
// Note: Format() overrides String() if both exist.
|
|
func (n Name) Format() string {
|
|
return fmt.Sprintf("%s %s", n.clean(n.First), n.clean(n.Last))
|
|
}
|
|
|
|
// clean ensures the first letter is capitalized and the rest are lowercase
|
|
func (n Name) clean(s string) string {
|
|
s = strings.TrimSpace(strings.ToLower(s))
|
|
words := strings.Fields(s)
|
|
s = strings.Join(words, "")
|
|
|
|
if s == "" {
|
|
return s
|
|
}
|
|
// Capitalize the first letter
|
|
runes := []rune(s)
|
|
runes[0] = unicode.ToUpper(runes[0])
|
|
return string(runes)
|
|
}
|
|
|
|
type Age int
|
|
|
|
// Age int will be ignore and string will be used
|
|
func (a Age) String() string {
|
|
return fmt.Sprintf("%d yrs", a)
|
|
}
|
|
|
|
func main() {
|
|
data := [][]any{
|
|
{Name{"Al i CE", " Ma SK"}, Age(25), "New York"},
|
|
{Name{"bOb", "mar le y"}, Age(30), "Boston"},
|
|
}
|
|
|
|
table := tablewriter.NewTable(os.Stdout,
|
|
tablewriter.WithRenderer(renderer.NewMarkdown()),
|
|
)
|
|
|
|
table.Header([]string{"Name", "Age", "City"})
|
|
table.Bulk(data)
|
|
table.Render()
|
|
}
|
|
```
|
|
|
|
**Output**:
|
|
|
|
```
|
|
| NAME | AGE | CITY |
|
|
|:----------:|:------:|:--------:|
|
|
| Alice Mask | 25 yrs | New York |
|
|
| Bob Marley | 30 yrs | Boston |
|
|
|
|
|
|
```
|
|
|
|
#### 3. CSV Input
|
|
|
|
Create a table from a CSV file with custom row alignment.
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"github.com/olekukonko/tablewriter"
|
|
"github.com/olekukonko/tablewriter/tw"
|
|
"log"
|
|
"os"
|
|
)
|
|
|
|
func main() {
|
|
// Assuming "test.csv" contains: "First Name,Last Name,SSN\nJohn,Barry,123456\nKathy,Smith,687987"
|
|
table, err := tablewriter.NewCSV(os.Stdout, "test.csv", true)
|
|
if err != nil {
|
|
log.Fatalf("Error: %v", err)
|
|
}
|
|
|
|
table.Configure(func(config *tablewriter.Config) {
|
|
config.Row.Alignment.Global = tw.AlignLeft
|
|
})
|
|
table.Render()
|
|
}
|
|
```
|
|
|
|
**Output**:
|
|
|
|
```
|
|
┌────────────┬───────────┬─────────┐
|
|
│ FIRST NAME │ LAST NAME │ SSN │
|
|
├────────────┼───────────┼─────────┤
|
|
│ John │ Barry │ 123456 │
|
|
│ Kathy │ Smith │ 687987 │
|
|
└────────────┴───────────┴─────────┘
|
|
```
|
|
|
|
### Advanced Examples
|
|
|
|
#### 4. Colorized Table with Long Values
|
|
|
|
Create a colorized table with wrapped long values, per-column colors, and a styled footer (inspired by `TestColorizedLongValues` and `TestColorizedCustomColors`).
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"github.com/fatih/color"
|
|
"github.com/olekukonko/tablewriter"
|
|
"github.com/olekukonko/tablewriter/renderer"
|
|
"github.com/olekukonko/tablewriter/tw"
|
|
"os"
|
|
)
|
|
|
|
func main() {
|
|
data := [][]string{
|
|
{"1", "This is a very long description that needs wrapping for readability", "OK"},
|
|
{"2", "Short description", "DONE"},
|
|
{"3", "Another lengthy description requiring truncation or wrapping", "ERROR"},
|
|
}
|
|
|
|
// Configure colors: green headers, cyan/magenta rows, yellow footer
|
|
colorCfg := renderer.ColorizedConfig{
|
|
Header: renderer.Tint{
|
|
FG: renderer.Colors{color.FgGreen, color.Bold}, // Green bold headers
|
|
BG: renderer.Colors{color.BgHiWhite},
|
|
},
|
|
Column: renderer.Tint{
|
|
FG: renderer.Colors{color.FgCyan}, // Default cyan for rows
|
|
Columns: []renderer.Tint{
|
|
{FG: renderer.Colors{color.FgMagenta}}, // Magenta for column 0
|
|
{}, // Inherit default (cyan)
|
|
{FG: renderer.Colors{color.FgHiRed}}, // High-intensity red for column 2
|
|
},
|
|
},
|
|
Footer: renderer.Tint{
|
|
FG: renderer.Colors{color.FgYellow, color.Bold}, // Yellow bold footer
|
|
Columns: []renderer.Tint{
|
|
{}, // Inherit default
|
|
{FG: renderer.Colors{color.FgHiYellow}}, // High-intensity yellow for column 1
|
|
{}, // Inherit default
|
|
},
|
|
},
|
|
Border: renderer.Tint{FG: renderer.Colors{color.FgWhite}}, // White borders
|
|
Separator: renderer.Tint{FG: renderer.Colors{color.FgWhite}}, // White separators
|
|
}
|
|
|
|
table := tablewriter.NewTable(os.Stdout,
|
|
tablewriter.WithRenderer(renderer.NewColorized(colorCfg)),
|
|
tablewriter.WithConfig(tablewriter.Config{
|
|
Row: tw.CellConfig{
|
|
Formatting: tw.CellFormatting{AutoWrap: tw.WrapNormal}, // Wrap long content
|
|
Alignment: tw.CellAlignment{Global: tw.AlignLeft}, // Left-align rows
|
|
ColMaxWidths: tw.CellWidth{Global: 25},
|
|
},
|
|
Footer: tw.CellConfig{
|
|
Alignment: tw.CellAlignment{Global: tw.AlignRight},
|
|
},
|
|
}),
|
|
)
|
|
|
|
table.Header([]string{"ID", "Description", "Status"})
|
|
table.Bulk(data)
|
|
table.Footer([]string{"", "Total", "3"})
|
|
table.Render()
|
|
}
|
|
```
|
|
|
|
**Output** (colors visible in ANSI-compatible terminals):
|
|
|
|

|
|
|
|
#### 5. Streaming Table with Truncation
|
|
|
|
Stream a table incrementally with truncation and a footer, simulating a real-time data feed (inspired by `TestOceanStreamTruncation` and `TestOceanStreamSlowOutput`).
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"github.com/olekukonko/tablewriter"
|
|
"github.com/olekukonko/tablewriter/tw"
|
|
"log"
|
|
"os"
|
|
"time"
|
|
)
|
|
|
|
func main() {
|
|
table := tablewriter.NewTable(os.Stdout, tablewriter.WithStreaming(tw.StreamConfig{Enable: true}))
|
|
|
|
// Start streaming
|
|
if err := table.Start(); err != nil {
|
|
log.Fatalf("Start failed: %v", err)
|
|
}
|
|
|
|
defer table.Close()
|
|
|
|
// Stream header
|
|
table.Header([]string{"ID", "Description", "Status"})
|
|
|
|
// Stream rows with simulated delay
|
|
data := [][]string{
|
|
{"1", "This description is too long", "OK"},
|
|
{"2", "Short desc", "DONE"},
|
|
{"3", "Another long description here", "ERROR"},
|
|
}
|
|
for _, row := range data {
|
|
table.Append(row)
|
|
time.Sleep(500 * time.Millisecond) // Simulate real-time data feed
|
|
}
|
|
|
|
// Stream footer
|
|
table.Footer([]string{"", "Total", "3"})
|
|
}
|
|
```
|
|
|
|
**Output** (appears incrementally):
|
|
|
|
```
|
|
┌────────┬───────────────┬──────────┐
|
|
│ ID │ DESCRIPTION │ STATUS │
|
|
├────────┼───────────────┼──────────┤
|
|
│ 1 │ This │ OK │
|
|
│ │ description │ │
|
|
│ │ is too long │ │
|
|
│ 2 │ Short desc │ DONE │
|
|
│ 3 │ Another long │ ERROR │
|
|
│ │ description │ │
|
|
│ │ here │ │
|
|
├────────┼───────────────┼──────────┤
|
|
│ │ Total │ 3 │
|
|
└────────┴───────────────┴──────────┘
|
|
```
|
|
|
|
**Note**: Long descriptions are truncated with `…` due to fixed column widths. The output appears row-by-row, simulating a real-time feed.
|
|
|
|
#### 6. Hierarchical Merging for Organizational Data
|
|
|
|
Show hierarchical merging for a tree-like structure, such as an organizational hierarchy (inspired by `TestMergeHierarchicalUnicode`).
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"github.com/olekukonko/tablewriter"
|
|
"github.com/olekukonko/tablewriter/renderer"
|
|
"github.com/olekukonko/tablewriter/tw"
|
|
"os"
|
|
)
|
|
|
|
func main() {
|
|
data := [][]string{
|
|
{"Engineering", "Backend", "API Team", "Alice"},
|
|
{"Engineering", "Backend", "Database Team", "Bob"},
|
|
{"Engineering", "Frontend", "UI Team", "Charlie"},
|
|
{"Marketing", "Digital", "SEO Team", "Dave"},
|
|
{"Marketing", "Digital", "Content Team", "Eve"},
|
|
}
|
|
|
|
table := tablewriter.NewTable(os.Stdout,
|
|
tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{
|
|
Settings: tw.Settings{Separators: tw.Separators{BetweenRows: tw.On}},
|
|
})),
|
|
tablewriter.WithConfig(tablewriter.Config{
|
|
Header: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignCenter}},
|
|
Row: tw.CellConfig{
|
|
Formatting: tw.CellFormatting{MergeMode: tw.MergeHierarchical},
|
|
Alignment: tw.CellAlignment{Global: tw.AlignLeft},
|
|
},
|
|
}),
|
|
)
|
|
table.Header([]string{"Department", "Division", "Team", "Lead"})
|
|
table.Bulk(data)
|
|
table.Render()
|
|
}
|
|
```
|
|
|
|
**Output**:
|
|
|
|
```
|
|
┌────────────┬──────────┬──────────────┬────────┐
|
|
│ DEPARTMENT │ DIVISION │ TEAM │ LEAD │
|
|
├────────────┼──────────┼──────────────┼────────┤
|
|
│ Engineering│ Backend │ API Team │ Alice │
|
|
│ │ ├──────────────┼────────┤
|
|
│ │ │ Database Team│ Bob │
|
|
│ │ Frontend ├──────────────┼────────┤
|
|
│ │ │ UI Team │ Charlie│
|
|
├────────────┼──────────┼──────────────┼────────┤
|
|
│ Marketing │ Digital │ SEO Team │ Dave │
|
|
│ │ ├──────────────┼────────┤
|
|
│ │ │ Content Team │ Eve │
|
|
└────────────┴──────────┴──────────────┴────────┘
|
|
```
|
|
|
|
**Note**: Hierarchical merging groups repeated values (e.g., "Engineering" spans multiple rows, "Backend" spans two teams), creating a tree-like structure.
|
|
|
|
#### 7. Custom Padding with Merging
|
|
|
|
Showcase custom padding and combined horizontal/vertical merging (inspired by `TestMergeWithPadding` in `merge_test.go`).
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"github.com/olekukonko/tablewriter"
|
|
"github.com/olekukonko/tablewriter/renderer"
|
|
"github.com/olekukonko/tablewriter/tw"
|
|
"os"
|
|
)
|
|
|
|
func main() {
|
|
data := [][]string{
|
|
{"1/1/2014", "Domain name", "Successful", "Successful"},
|
|
{"1/1/2014", "Domain name", "Pending", "Waiting"},
|
|
{"1/1/2014", "Domain name", "Successful", "Rejected"},
|
|
{"", "", "TOTAL", "$145.93"},
|
|
}
|
|
|
|
table := tablewriter.NewTable(os.Stdout,
|
|
tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{
|
|
Settings: tw.Settings{Separators: tw.Separators{BetweenRows: tw.On}},
|
|
})),
|
|
tablewriter.WithConfig(tablewriter.Config{
|
|
Row: tw.CellConfig{
|
|
Formatting: tw.CellFormatting{MergeMode: tw.MergeBoth},
|
|
Alignment: tw.CellAlignment{PerColumn: []tw.Align{tw.Skip, tw.Skip, tw.AlignRight, tw.AlignLeft}},
|
|
},
|
|
|
|
Footer: tw.CellConfig{
|
|
Padding: tw.CellPadding{
|
|
Global: tw.Padding{Left: "*", Right: "*"},
|
|
PerColumn: []tw.Padding{{}, {}, {Bottom: "^"}, {Bottom: "^"}},
|
|
},
|
|
Alignment: tw.CellAlignment{PerColumn: []tw.Align{tw.Skip, tw.Skip, tw.AlignRight, tw.AlignLeft}},
|
|
},
|
|
}),
|
|
)
|
|
table.Header([]string{"Date", "Description", "Status", "Conclusion"})
|
|
table.Bulk(data)
|
|
table.Render()
|
|
}
|
|
```
|
|
|
|
**Output**:
|
|
|
|
```
|
|
┌──────────┬─────────────┬────────────┬────────────┐
|
|
│ DATE │ DESCRIPTION │ STATUS │ CONCLUSION │
|
|
├──────────┼─────────────┼────────────┴────────────┤
|
|
│ 1/1/2014 │ Domain name │ Successful │
|
|
│ │ ├────────────┬────────────┤
|
|
│ │ │ Pending │ Waiting │
|
|
│ │ ├────────────┼────────────┤
|
|
│ │ │ Successful │ Rejected │
|
|
├──────────┼─────────────┼────────────┼────────────┤
|
|
│ │ │ TOTAL │ $145.93 │
|
|
│ │ │^^^^^^^^^^^^│^^^^^^^^^^^^│
|
|
└──────────┴─────────────┴────────────┴────────────┘
|
|
```
|
|
|
|
#### 8. Nested Tables
|
|
|
|
Create a table with nested sub-tables for complex layouts (inspired by `TestMasterClass` in `extra_test.go`).
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"github.com/olekukonko/tablewriter"
|
|
"github.com/olekukonko/tablewriter/renderer"
|
|
"github.com/olekukonko/tablewriter/tw"
|
|
"os"
|
|
)
|
|
|
|
func main() {
|
|
// Helper to create a sub-table
|
|
createSubTable := func(s string) string {
|
|
var buf bytes.Buffer
|
|
table := tablewriter.NewTable(&buf,
|
|
tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{
|
|
Borders: tw.BorderNone,
|
|
Symbols: tw.NewSymbols(tw.StyleASCII),
|
|
Settings: tw.Settings{
|
|
Separators: tw.Separators{BetweenRows: tw.On},
|
|
Lines: tw.Lines{ShowFooterLine: tw.On},
|
|
},
|
|
})),
|
|
tablewriter.WithConfig(tablewriter.Config{
|
|
MaxWidth: 10,
|
|
Row: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignCenter}},
|
|
}),
|
|
)
|
|
table.Append([]string{s, s})
|
|
table.Append([]string{s, s})
|
|
table.Render()
|
|
return buf.String()
|
|
}
|
|
|
|
// Main table
|
|
table := tablewriter.NewTable(os.Stdout,
|
|
tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{
|
|
Borders: tw.BorderNone,
|
|
Settings: tw.Settings{Separators: tw.Separators{BetweenColumns: tw.On}},
|
|
})),
|
|
tablewriter.WithConfig(tablewriter.Config{
|
|
MaxWidth: 30,
|
|
Row: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignCenter}},
|
|
}),
|
|
)
|
|
table.Append([]string{createSubTable("A"), createSubTable("B")})
|
|
table.Append([]string{createSubTable("C"), createSubTable("D")})
|
|
table.Render()
|
|
}
|
|
```
|
|
|
|
**Output**:
|
|
|
|
```
|
|
A | A │ B | B
|
|
---+--- │ ---+---
|
|
A | A │ B | B
|
|
C | C │ D | D
|
|
---+--- │ ---+---
|
|
C | C │ D | D
|
|
```
|
|
|
|
#### 9. Structs with Database
|
|
|
|
Render a table from a slice of structs, simulating a database query (inspired by `TestStructTableWithDB` in `struct_test.go`).
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/olekukonko/tablewriter"
|
|
"github.com/olekukonko/tablewriter/renderer"
|
|
"github.com/olekukonko/tablewriter/tw"
|
|
"os"
|
|
)
|
|
|
|
type Employee struct {
|
|
ID int
|
|
Name string
|
|
Age int
|
|
Department string
|
|
Salary float64
|
|
}
|
|
|
|
func employeeStringer(e interface{}) []string {
|
|
emp, ok := e.(Employee)
|
|
if !ok {
|
|
return []string{"Error: Invalid type"}
|
|
}
|
|
return []string{
|
|
fmt.Sprintf("%d", emp.ID),
|
|
emp.Name,
|
|
fmt.Sprintf("%d", emp.Age),
|
|
emp.Department,
|
|
fmt.Sprintf("%.2f", emp.Salary),
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
employees := []Employee{
|
|
{ID: 1, Name: "Alice Smith", Age: 28, Department: "Engineering", Salary: 75000.50},
|
|
{ID: 2, Name: "Bob Johnson", Age: 34, Department: "Marketing", Salary: 62000.00},
|
|
{ID: 3, Name: "Charlie Brown", Age: 45, Department: "HR", Salary: 80000.75},
|
|
}
|
|
|
|
table := tablewriter.NewTable(os.Stdout,
|
|
tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{
|
|
Symbols: tw.NewSymbols(tw.StyleRounded),
|
|
})),
|
|
tablewriter.WithStringer(employeeStringer),
|
|
tablewriter.WithConfig(tablewriter.Config{
|
|
Header: tw.CellConfig{
|
|
Formatting: tw.CellFormatting{AutoFormat: tw.On},
|
|
Alignment: tw.CellAlignment{Global: tw.AlignCenter},
|
|
},
|
|
Row: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignLeft}},
|
|
Footer: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignRight}},
|
|
}),
|
|
)
|
|
table.Header([]string{"ID", "Name", "Age", "Department", "Salary"})
|
|
|
|
for _, emp := range employees {
|
|
table.Append(emp)
|
|
}
|
|
|
|
totalSalary := 0.0
|
|
for _, emp := range employees {
|
|
totalSalary += emp.Salary
|
|
}
|
|
table.Footer([]string{"", "", "", "Total", fmt.Sprintf("%.2f", totalSalary)})
|
|
table.Render()
|
|
}
|
|
```
|
|
|
|
**Output**:
|
|
|
|
```
|
|
╭────┬───────────────┬─────┬─────────────┬───────────╮
|
|
│ ID │ NAME │ AGE │ DEPARTMENT │ SALARY │
|
|
├────┼───────────────┼─────┼─────────────┼───────────┤
|
|
│ 1 │ Alice Smith │ 28 │ Engineering │ 75000.50 │
|
|
│ 2 │ Bob Johnson │ 34 │ Marketing │ 62000.00 │
|
|
│ 3 │ Charlie Brown │ 45 │ HR │ 80000.75 │
|
|
├────┼───────────────┼─────┼─────────────┼───────────┤
|
|
│ │ │ │ Total │ 217001.25 │
|
|
╰────┴───────────────┴─────┴─────────────┴───────────╯
|
|
```
|
|
|
|
|
|
#### 10. Simple Html Table
|
|
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"github.com/olekukonko/tablewriter"
|
|
"github.com/olekukonko/tablewriter/renderer"
|
|
"github.com/olekukonko/tablewriter/tw"
|
|
"os"
|
|
)
|
|
|
|
func main() {
|
|
data := [][]string{
|
|
{"North", "Q1 & Q2", "Q1 & Q2", "$2200.00"},
|
|
{"South", "Q1", "Q1", "$1000.00"},
|
|
{"South", "Q2", "Q2", "$1200.00"},
|
|
}
|
|
|
|
// Configure HTML with custom CSS classes and content escaping
|
|
htmlCfg := renderer.HTMLConfig{
|
|
TableClass: "sales-table",
|
|
HeaderClass: "table-header",
|
|
BodyClass: "table-body",
|
|
FooterClass: "table-footer",
|
|
RowClass: "table-row",
|
|
HeaderRowClass: "header-row",
|
|
FooterRowClass: "footer-row",
|
|
EscapeContent: true, // Escape HTML characters (e.g., "&" to "&")
|
|
}
|
|
|
|
table := tablewriter.NewTable(os.Stdout,
|
|
tablewriter.WithRenderer(renderer.NewHTML(htmlCfg)),
|
|
tablewriter.WithConfig(tablewriter.Config{
|
|
Header: tw.CellConfig{
|
|
Formatting: tw.CellFormatting{MergeMode: tw.MergeHorizontal}, // Merge identical header cells
|
|
Alignment: tw.CellAlignment{Global: tw.AlignCenter},
|
|
},
|
|
Row: tw.CellConfig{
|
|
Formatting: tw.CellFormatting{MergeMode: tw.MergeHorizontal}, // Merge identical row cells
|
|
Alignment: tw.CellAlignment{Global: tw.AlignLeft},
|
|
},
|
|
Footer: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignRight}},
|
|
}),
|
|
)
|
|
|
|
table.Header([]string{"Region", "Quarter", "Quarter", "Sales"})
|
|
table.Bulk(data)
|
|
table.Footer([]string{"", "", "Total", "$4400.00"})
|
|
table.Render()
|
|
}
|
|
```
|
|
|
|
**Output**:
|
|
|
|
```
|
|
<table class="sales-table">
|
|
<thead class="table-header">
|
|
<tr class="header-row">
|
|
<th style="text-align: center;">REGION</th>
|
|
<th colspan="2" style="text-align: center;">QUARTER</th>
|
|
<th style="text-align: center;">SALES</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="table-body">
|
|
<tr class="table-row">
|
|
<td style="text-align: left;">North</td>
|
|
<td colspan="2" style="text-align: left;">Q1 & Q2</td>
|
|
<td style="text-align: left;">$2200.00</td>
|
|
</tr>
|
|
<tr class="table-row">
|
|
<td style="text-align: left;">South</td>
|
|
<td colspan="2" style="text-align: left;">Q1</td>
|
|
<td style="text-align: left;">$1000.00</td>
|
|
</tr>
|
|
<tr class="table-row">
|
|
<td style="text-align: left;">South</td>
|
|
<td colspan="2" style="text-align: left;">Q2</td>
|
|
<td style="text-align: left;">$1200.00</td>
|
|
</tr>
|
|
</tbody>
|
|
<tfoot class="table-footer">
|
|
<tr class="footer-row">
|
|
<td style="text-align: right;"></td>
|
|
<td style="text-align: right;"></td>
|
|
<td style="text-align: right;">Total</td>
|
|
<td style="text-align: right;">$4400.00</td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
|
|
```
|
|
|
|
#### 11. SVG Support
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/olekukonko/ll"
|
|
"github.com/olekukonko/tablewriter"
|
|
"github.com/olekukonko/tablewriter/renderer"
|
|
"os"
|
|
)
|
|
|
|
type Age int
|
|
|
|
func (a Age) String() string {
|
|
return fmt.Sprintf("%d yrs", a)
|
|
}
|
|
|
|
func main() {
|
|
data := [][]any{
|
|
{"Alice", Age(25), "New York"},
|
|
{"Bob", Age(30), "Boston"},
|
|
}
|
|
|
|
file, err := os.OpenFile("out.svg", os.O_CREATE|os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
ll.Fatal(err)
|
|
}
|
|
defer file.Close()
|
|
|
|
table := tablewriter.NewTable(file, tablewriter.WithRenderer(renderer.NewSVG()))
|
|
table.Header("Name", "Age", "City")
|
|
table.Bulk(data)
|
|
table.Render()
|
|
}
|
|
```
|
|
|
|
```go
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="170.80" height="84.40" font-family="sans-serif" font-size="12.00">
|
|
<style>text { stroke: none; }</style>
|
|
<rect x="1.00" y="1.00" width="46.00" height="26.80" fill="#F0F0F0"/>
|
|
<text x="24.00" y="14.40" fill="black" text-anchor="middle" dominant-baseline="middle">NAME</text>
|
|
<rect x="48.00" y="1.00" width="53.20" height="26.80" fill="#F0F0F0"/>
|
|
<text x="74.60" y="14.40" fill="black" text-anchor="middle" dominant-baseline="middle">AGE</text>
|
|
<rect x="102.20" y="1.00" width="67.60" height="26.80" fill="#F0F0F0"/>
|
|
<text x="136.00" y="14.40" fill="black" text-anchor="middle" dominant-baseline="middle">CITY</text>
|
|
<rect x="1.00" y="28.80" width="46.00" height="26.80" fill="white"/>
|
|
<text x="6.00" y="42.20" fill="black" text-anchor="start" dominant-baseline="middle">Alice</text>
|
|
<rect x="48.00" y="28.80" width="53.20" height="26.80" fill="white"/>
|
|
<text x="53.00" y="42.20" fill="black" text-anchor="start" dominant-baseline="middle">25 yrs</text>
|
|
<rect x="102.20" y="28.80" width="67.60" height="26.80" fill="white"/>
|
|
<text x="107.20" y="42.20" fill="black" text-anchor="start" dominant-baseline="middle">New York</text>
|
|
<rect x="1.00" y="56.60" width="46.00" height="26.80" fill="#F9F9F9"/>
|
|
<text x="6.00" y="70.00" fill="black" text-anchor="start" dominant-baseline="middle">Bob</text>
|
|
<rect x="48.00" y="56.60" width="53.20" height="26.80" fill="#F9F9F9"/>
|
|
<text x="53.00" y="70.00" fill="black" text-anchor="start" dominant-baseline="middle">30 yrs</text>
|
|
<rect x="102.20" y="56.60" width="67.60" height="26.80" fill="#F9F9F9"/>
|
|
<text x="107.20" y="70.00" fill="black" text-anchor="start" dominant-baseline="middle">Boston</text>
|
|
<g class="table-borders" stroke="black" stroke-width="1.00" stroke-linecap="square">
|
|
<line x1="0.50" y1="0.50" x2="170.30" y2="0.50" />
|
|
<line x1="0.50" y1="28.30" x2="170.30" y2="28.30" />
|
|
<line x1="0.50" y1="56.10" x2="170.30" y2="56.10" />
|
|
<line x1="0.50" y1="83.90" x2="170.30" y2="83.90" />
|
|
<line x1="0.50" y1="0.50" x2="0.50" y2="83.90" />
|
|
<line x1="47.50" y1="0.50" x2="47.50" y2="83.90" />
|
|
<line x1="101.70" y1="0.50" x2="101.70" y2="83.90" />
|
|
<line x1="170.30" y1="0.50" x2="170.30" y2="83.90" />
|
|
</g>
|
|
</svg>
|
|
|
|
```
|
|
|
|
#### 12 Simple Application
|
|
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/olekukonko/tablewriter"
|
|
"github.com/olekukonko/tablewriter/tw"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
folder = "📁"
|
|
file = "📄"
|
|
baseDir = "../"
|
|
indentStr = " "
|
|
)
|
|
|
|
func main() {
|
|
table := tablewriter.NewTable(os.Stdout, tablewriter.WithTrimSpace(tw.Off))
|
|
table.Header([]string{"Tree", "Size", "Permissions", "Modified"})
|
|
err := filepath.WalkDir(baseDir, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if d.Name() == "." || d.Name() == ".." {
|
|
return nil
|
|
}
|
|
|
|
// Calculate relative path depth
|
|
relPath, err := filepath.Rel(baseDir, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
depth := 0
|
|
if relPath != "." {
|
|
depth = len(strings.Split(relPath, string(filepath.Separator))) - 1
|
|
}
|
|
|
|
indent := strings.Repeat(indentStr, depth)
|
|
|
|
var name string
|
|
if d.IsDir() {
|
|
name = fmt.Sprintf("%s%s %s", indent, folder, d.Name())
|
|
} else {
|
|
name = fmt.Sprintf("%s%s %s", indent, file, d.Name())
|
|
}
|
|
|
|
info, err := d.Info()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
table.Append([]string{
|
|
name,
|
|
Size(info.Size()).String(),
|
|
info.Mode().String(),
|
|
Time(info.ModTime()).Format(),
|
|
})
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stdout, "Error: %v\n", err)
|
|
return
|
|
}
|
|
|
|
table.Render()
|
|
}
|
|
|
|
const (
|
|
KB = 1024
|
|
MB = KB * 1024
|
|
GB = MB * 1024
|
|
TB = GB * 1024
|
|
)
|
|
|
|
type Size int64
|
|
|
|
func (s Size) String() string {
|
|
switch {
|
|
case s < KB:
|
|
return fmt.Sprintf("%d B", s)
|
|
case s < MB:
|
|
return fmt.Sprintf("%.2f KB", float64(s)/KB)
|
|
case s < GB:
|
|
return fmt.Sprintf("%.2f MB", float64(s)/MB)
|
|
case s < TB:
|
|
return fmt.Sprintf("%.2f GB", float64(s)/GB)
|
|
default:
|
|
return fmt.Sprintf("%.2f TB", float64(s)/TB)
|
|
}
|
|
}
|
|
|
|
type Time time.Time
|
|
|
|
func (t Time) Format() string {
|
|
now := time.Now()
|
|
diff := now.Sub(time.Time(t))
|
|
|
|
if diff.Seconds() < 60 {
|
|
return "just now"
|
|
} else if diff.Minutes() < 60 {
|
|
return fmt.Sprintf("%d minutes ago", int(diff.Minutes()))
|
|
} else if diff.Hours() < 24 {
|
|
return fmt.Sprintf("%d hours ago", int(diff.Hours()))
|
|
} else if diff.Hours() < 24*7 {
|
|
return fmt.Sprintf("%d days ago", int(diff.Hours()/24))
|
|
} else {
|
|
return time.Time(t).Format("Jan 2, 2006")
|
|
}
|
|
}
|
|
|
|
```
|
|
|
|
```
|
|
┌──────────────────┬─────────┬─────────────┬──────────────┐
|
|
│ TREE │ SIZE │ PERMISSIONS │ MODIFIED │
|
|
├──────────────────┼─────────┼─────────────┼──────────────┤
|
|
│ 📁 filetable │ 160 B │ drwxr-xr-x │ just now │
|
|
│ 📄 main.go │ 2.19 KB │ -rw-r--r-- │ 22 hours ago │
|
|
│ 📄 out.txt │ 0 B │ -rw-r--r-- │ just now │
|
|
│ 📁 testdata │ 128 B │ drwxr-xr-x │ 1 days ago │
|
|
│ 📄 a.txt │ 11 B │ -rw-r--r-- │ 1 days ago │
|
|
│ 📄 b.txt │ 17 B │ -rw-r--r-- │ 1 days ago │
|
|
│ 📁 symbols │ 128 B │ drwxr-xr-x │ just now │
|
|
│ 📄 main.go │ 4.58 KB │ -rw-r--r-- │ 1 hours ago │
|
|
│ 📄 out.txt │ 8.72 KB │ -rw-r--r-- │ just now │
|
|
└──────────────────┴─────────┴─────────────┴──────────────┘
|
|
```
|
|
|
|
|
|
## Changes
|
|
|
|
- `AutoFormat` changes See [#261](https://github.com/olekukonko/tablewriter/issues/261)
|
|
|
|
|
|
## Command-Line Tool
|
|
|
|
The `csv2table` tool converts CSV files to ASCII tables. See `cmd/csv2table/csv2table.go` for details.
|
|
|
|
Example usage:
|
|
|
|
```bash
|
|
csv2table -f test.csv -h true -a left
|
|
```
|
|
|
|
## Contributing
|
|
|
|
Contributions are welcome! Submit issues or pull requests to the [GitHub repository](https://github.com/olekukonko/tablewriter).
|
|
|
|
## License
|
|
|
|
MIT License. See the [LICENSE](LICENSE) file for details. |