AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
翻译准备中
Golang 中的 ASCII 表格
AI 翻译
ASCII table in golang
开发工具Go · HCLGitHub @olekukonko自行安装运营者未确认
试用过了吗? 点一下就好,无需登录。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
安装型项目无需部署域名也可验证。个人仓库可通过 GitHub 登录验证,组织仓库可使用上方的仓库验证文件。
制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗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.
For use with legacy applications:
go get github.com/olekukonko/[email protected]
The latest stable version
go get github.com/olekukonko/[email protected]
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
sql.Null* typespackage main
import (
"github.com/olekukonko/tablewriter"
"os"
)
func main() {
data := [][]string{
{"Package", "Version", "Status"},
{"tablewriter", "v0.0.5", "legacy"},
{"tablewriter", "v1.1.5", "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.1.5 │ latest │
└─────────────┴─────────┴────────┘
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
Rendition - Defines how a renderer formats tables and contains the complete visual styling configuration
These components can be configured with various tablewriter.With*() functional options when creating a new table.
Create a basic table with headers and rows.
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 │
└───────┴────────┴──────────┘
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 for more
Generate a Markdown table for documentation.
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 |
Create a table from a CSV file with custom row alignment.
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 │
└────────────┴───────────┴─────────┘
Create a colorized table with wrapped long values, per-column colors, and a styled footer (inspired by TestColorizedLongValues and TestColorizedCustomColors).
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):
Besides the named color.Fg*/color.Bg* attributes, tints accept 24-bit
colors. renderer.RGB and renderer.BgRGB take red, green and blue channels
(0-255, out-of-range values are clamped), while renderer.Hex/renderer.BgHex
parse a #RRGGBB or #RGB string. All of them return a renderer.Colors, so
they slot in wherever named attributes do and can be combined with append:
orange, _ := renderer.Hex("#ff8800")
colorCfg := renderer.ColorizedConfig{
// Bold orange headers on a dark-grey background.
Header: renderer.Tint{
FG: append(orange, color.Bold),
BG: renderer.BgRGB(30, 30, 30),
},
// Teal rows.
Column: renderer.Tint{FG: renderer.RGB(0, 200, 180)},
}
Terminals without true-color support may approximate or ignore these colors.
Stream a table incrementally with truncation and a footer, simulating a real-time data feed (inspired by TestOceanStreamTruncation and TestOceanStreamSlowOutput).
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.
Show hierarchical merging for a tree-like structure, such as an organizational hierarchy (inspired by TestMergeHierarchicalUnicode).
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{
Merging: tw.CellMerging{Mode: 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.
Showcase custom padding and combined horizontal/vertical merging (inspired by TestMergeWithPadding in merge_test.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{
Merging: tw.CellMerging{Mode: 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 │
│ │ │^^^^^^^^^^^^│^^^^^^^^^^^^│
└──────────┴─────────────┴────────────┴────────────┘
Create a table with nested sub-tables for complex layouts (inspired by TestMasterClass in extra_test.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
Render a table from a slice of structs, simulating a database query (inspired by TestStructTableWithDB in struct_test.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 │
╰────┴───────────────┴─────┴─────────────┴───────────╯
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{
Merging: tw.CellMerging{Mode: tw.MergeHorizontal}, // Merge identical header cells
Alignment: tw.CellAlignment{Global: tw.AlignCenter},
},
Row: tw.CellConfig{
Merging: tw.CellMerging{Mode: 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>
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()
}
<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>
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 │
└──────────────────┴─────────┴─────────────┴──────────────┘
AutoFormat changes See #261Counting changes See #294The csv2table tool converts CSV files to ASCII tables. See cmd/csv2table/csv2table.go for details.
Example usage:
csv2table -f test.csv -h true -a left
Contributions are welcome! Submit issues or pull requests to the GitHub repository.
MIT License. See the LICENSE file for details.