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.
- 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
For use with legacy applications:
go get github.com/olekukonko/tablewriter@v0.0.5The latest stable version
go get github.com/olekukonko/tablewriter@v1.1.4Warning: 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
- 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
package main import ( "github.com/olekukonko/tablewriter" "os" ) func main() { data := [][]string{ {"Package", "Version", "Status"}, {"tablewriter", "v0.0.5", "legacy"}, {"tablewriter", "v1.1.4", "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.4 │ 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
- 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.
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):
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 │ └──────────────────┴─────────┴─────────────┴──────────────┘ AutoFormatchanges See #261
Countingchanges See #294
The csv2table tool converts CSV files to ASCII tables. See cmd/csv2table/csv2table.go for details.
Example usage:
csv2table -f test.csv -h true -a leftContributions are welcome! Submit issues or pull requests to the GitHub repository.
MIT License. See the LICENSE file for details.
