pax_global_header00006660000000000000000000000064142764507170014527gustar00rootroot0000000000000052 comment=fce553e800176197145acff5e65cafe07c2cb650 table-1.8.0/000077500000000000000000000000001427645071700126245ustar00rootroot00000000000000table-1.8.0/.github/000077500000000000000000000000001427645071700141645ustar00rootroot00000000000000table-1.8.0/.github/workflows/000077500000000000000000000000001427645071700162215ustar00rootroot00000000000000table-1.8.0/.github/workflows/lint.yml000066400000000000000000000005501427645071700177120ustar00rootroot00000000000000name: golangci-lint on: push: branches: - main pull_request: permissions: contents: read pull-requests: read jobs: golangci: name: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: golangci-lint uses: golangci/golangci-lint-action@v2 with: args: --timeout 3m --verbose table-1.8.0/.github/workflows/test.yml000066400000000000000000000012141427645071700177210ustar00rootroot00000000000000name: test on: push: branches: - main pull_request: jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: os: [ ubuntu-latest ] # optionally add macos-latest, windows-latest name: build and test steps: - uses: actions/checkout@v2 - uses: actions/setup-go@v2 with: go-version: '1.18' - uses: actions/cache@v3 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: Run test run: make testtable-1.8.0/LICENSE000066400000000000000000000020561427645071700136340ustar00rootroot00000000000000MIT License Copyright (c) 2022 Aqua Security Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. table-1.8.0/Makefile000066400000000000000000000001601427645071700142610ustar00rootroot00000000000000default: test .PHONY: test test: go test -v -race -cover ./... .PHONY: readme readme: go run ./cmd/examples table-1.8.0/README.md000066400000000000000000000466421427645071700141170ustar00rootroot00000000000000# table: Tables for terminals This is a Go module for rendering tables in the terminal. ![A fruity demonstration table](./_examples/99-ansi/screenshot.png) ## Features - :arrow_up_down: Headers/footers - :leftwards_arrow_with_hook: Text wrapping - :twisted_rightwards_arrows: Auto-merging of cells - :interrobang: Customisable line/border characters - :rainbow: Customisable line/border colours - :play_or_pause_button: Individually enable/disable borders, row lines - :left_right_arrow: Set alignments on a per-column basis, with separate settings for headers/footers - :triangular_ruler: Intelligently wrap/pad/measure ANSI coloured input - :dancers: Support for double-width unicode characters - :bar_chart: Load data from CSV files Check out the [documentation](https://pkg.go.dev/github.com/aquasecurity/table) for full features/usage. ## Examples ### Example: Basic ```go package main import ( "os" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetHeaders("ID", "Fruit", "Stock") t.AddRow("1", "Apple", "14") t.AddRow("2", "Banana", "88,041") t.AddRow("3", "Cherry", "342") t.AddRow("4", "Dragonfruit", "1") t.Render() } ``` #### Output ``` ┌────┬─────────────┬────────┐ │ ID │ Fruit │ Stock │ ├────┼─────────────┼────────┤ │ 1 │ Apple │ 14 │ ├────┼─────────────┼────────┤ │ 2 │ Banana │ 88,041 │ ├────┼─────────────┼────────┤ │ 3 │ Cherry │ 342 │ ├────┼─────────────┼────────┤ │ 4 │ Dragonfruit │ 1 │ └────┴─────────────┴────────┘ ``` ### Example: No Row Lines ```go package main import ( "os" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetRowLines(false) t.SetHeaders("ID", "Fruit", "Stock") t.AddRow("1", "Apple", "14") t.AddRow("2", "Banana", "88,041") t.AddRow("3", "Cherry", "342") t.AddRow("4", "Dragonfruit", "1") t.Render() } ``` #### Output ``` ┌────┬─────────────┬────────┐ │ ID │ Fruit │ Stock │ ├────┼─────────────┼────────┤ │ 1 │ Apple │ 14 │ │ 2 │ Banana │ 88,041 │ │ 3 │ Cherry │ 342 │ │ 4 │ Dragonfruit │ 1 │ └────┴─────────────┴────────┘ ``` ### Example: No Borders ```go package main import ( "os" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetBorders(false) t.SetHeaders("ID", "Fruit", "Stock") t.AddRow("1", "Apple", "14") t.AddRow("2", "Banana", "88,041") t.AddRow("3", "Cherry", "342") t.AddRow("4", "Dragonfruit", "1") t.Render() } ``` #### Output ``` ID │ Fruit │ Stock ────┼─────────────┼──────── 1 │ Apple │ 14 ────┼─────────────┼──────── 2 │ Banana │ 88,041 ────┼─────────────┼──────── 3 │ Cherry │ 342 ────┼─────────────┼──────── 4 │ Dragonfruit │ 1 ``` ### Example: No Borders Or Row Lines ```go package main import ( "os" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetRowLines(false) t.SetBorders(false) t.SetHeaders("ID", "Fruit", "Stock") t.AddRow("1", "Apple", "14") t.AddRow("2", "Banana", "88,041") t.AddRow("3", "Cherry", "342") t.AddRow("4", "Dragonfruit", "1") t.Render() } ``` #### Output ``` ID │ Fruit │ Stock ────┼─────────────┼──────── 1 │ Apple │ 14 2 │ Banana │ 88,041 3 │ Cherry │ 342 4 │ Dragonfruit │ 1 ``` ### Example: Specific Borders ```go package main import ( "os" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetRowLines(false) t.SetBorderLeft(true) t.SetBorderRight(false) t.SetBorderTop(true) t.SetBorderBottom(false) t.SetHeaders("ID", "Fruit", "Stock") t.AddRow("1", "Apple", "14") t.AddRow("2", "Banana", "88,041") t.AddRow("3", "Cherry", "342") t.AddRow("4", "Dragonfruit", "1") t.Render() } ``` #### Output ``` ┌────┬─────────────┬──────── │ ID │ Fruit │ Stock ├────┼─────────────┼──────── │ 1 │ Apple │ 14 │ 2 │ Banana │ 88,041 │ 3 │ Cherry │ 342 │ 4 │ Dragonfruit │ 1 ``` ### Example: Footers ```go package main import ( "os" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetHeaders("ID", "Fruit", "Stock") t.SetFooters("ID", "Fruit", "Stock") t.AddRow("1", "Apple", "14") t.AddRow("2", "Banana", "88,041") t.AddRow("3", "Cherry", "342") t.AddRow("4", "Dragonfruit", "1") t.Render() } ``` #### Output ``` ┌────┬─────────────┬────────┐ │ ID │ Fruit │ Stock │ ├────┼─────────────┼────────┤ │ 1 │ Apple │ 14 │ ├────┼─────────────┼────────┤ │ 2 │ Banana │ 88,041 │ ├────┼─────────────┼────────┤ │ 3 │ Cherry │ 342 │ ├────┼─────────────┼────────┤ │ 4 │ Dragonfruit │ 1 │ ├────┼─────────────┼────────┤ │ ID │ Fruit │ Stock │ └────┴─────────────┴────────┘ ``` ### Example: Padding ```go package main import ( "os" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetPadding(5) t.SetHeaders("ID", "Fruit", "Stock") t.AddRow("1", "Apple", "14") t.AddRow("2", "Banana", "88,041") t.AddRow("3", "Cherry", "342") t.AddRow("4", "Dragonfruit", "1") t.Render() } ``` #### Output ``` ┌────────────┬─────────────────────┬────────────────┐ │ ID │ Fruit │ Stock │ ├────────────┼─────────────────────┼────────────────┤ │ 1 │ Apple │ 14 │ ├────────────┼─────────────────────┼────────────────┤ │ 2 │ Banana │ 88,041 │ ├────────────┼─────────────────────┼────────────────┤ │ 3 │ Cherry │ 342 │ ├────────────┼─────────────────────┼────────────────┤ │ 4 │ Dragonfruit │ 1 │ └────────────┴─────────────────────┴────────────────┘ ``` ### Example: Alignment ```go package main import ( "os" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetAlignment(table.AlignLeft, table.AlignCenter, table.AlignRight) t.SetHeaders("ID", "Fruit", "Stock") t.AddRow("1", "Apple", "14") t.AddRow("2", "Banana", "88,041") t.AddRow("3", "Cherry", "342") t.AddRow("4", "Dragonfruit", "1") t.Render() } ``` #### Output ``` ┌────┬─────────────┬────────┐ │ ID │ Fruit │ Stock │ ├────┼─────────────┼────────┤ │ 1 │ Apple │ 14 │ ├────┼─────────────┼────────┤ │ 2 │ Banana │ 88,041 │ ├────┼─────────────┼────────┤ │ 3 │ Cherry │ 342 │ ├────┼─────────────┼────────┤ │ 4 │ Dragonfruit │ 1 │ └────┴─────────────┴────────┘ ``` ### Example: Rounded Corners ```go package main import ( "os" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetDividers(table.UnicodeRoundedDividers) t.SetHeaders("ID", "Fruit", "Stock") t.AddRow("1", "Apple", "14") t.AddRow("2", "Banana", "88,041") t.AddRow("3", "Cherry", "342") t.AddRow("4", "Dragonfruit", "1") t.Render() } ``` #### Output ``` ╭────┬─────────────┬────────╮ │ ID │ Fruit │ Stock │ ├────┼─────────────┼────────┤ │ 1 │ Apple │ 14 │ ├────┼─────────────┼────────┤ │ 2 │ Banana │ 88,041 │ ├────┼─────────────┼────────┤ │ 3 │ Cherry │ 342 │ ├────┼─────────────┼────────┤ │ 4 │ Dragonfruit │ 1 │ ╰────┴─────────────┴────────╯ ``` ### Example: Custom Dividers ```go package main import ( "os" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetDividers(table.Dividers{ ALL: "@", NES: "@", NSW: "@", NEW: "@", ESW: "@", NE: "@", NW: "@", SW: "@", ES: "@", EW: "~", NS: "!", }) t.SetHeaders("ID", "Fruit", "Stock") t.AddRow("1", "Apple", "14") t.AddRow("2", "Banana", "88,041") t.AddRow("3", "Cherry", "342") t.AddRow("4", "Dragonfruit", "1") t.Render() } ``` #### Output ``` @~~~~@~~~~~~~~~~~~~@~~~~~~~~@ ! ID ! Fruit ! Stock ! @~~~~@~~~~~~~~~~~~~@~~~~~~~~@ ! 1 ! Apple ! 14 ! @~~~~@~~~~~~~~~~~~~@~~~~~~~~@ ! 2 ! Banana ! 88,041 ! @~~~~@~~~~~~~~~~~~~@~~~~~~~~@ ! 3 ! Cherry ! 342 ! @~~~~@~~~~~~~~~~~~~@~~~~~~~~@ ! 4 ! Dragonfruit ! 1 ! @~~~~@~~~~~~~~~~~~~@~~~~~~~~@ ``` ### Example: Auto Merge Cells ```go package main import ( "os" "time" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetAutoMerge(true) t.SetHeaders("System", "Status", "Last Check") t.AddRow("Life Support", "OK", time.Now().Format(time.Stamp)) t.AddRow("Nuclear Generator", "OK", time.Now().Add(-time.Minute).Format(time.Stamp)) t.AddRow("Weapons Systems", "FAIL", time.Now().Format(time.Stamp)) t.AddRow("Shields", "OK", time.Now().Format(time.Stamp)) t.Render() } ``` #### Output ``` ┌───────────────────┬────────┬─────────────────┐ │ System │ Status │ Last Check │ ├───────────────────┼────────┼─────────────────┤ │ Life Support │ OK │ May 13 17:34:32 │ ├───────────────────┤ ├─────────────────┤ │ Nuclear Generator │ │ May 13 17:33:32 │ ├───────────────────┼────────┼─────────────────┤ │ Weapons Systems │ FAIL │ May 13 17:34:32 │ ├───────────────────┼────────┤ │ │ Shields │ OK │ │ └───────────────────┴────────┴─────────────────┘ ``` ### Example: Load Data From Csv ```go package main import ( "os" "github.com/aquasecurity/table" ) func main() { f, err := os.Open("./_examples/12-load-data-from-csv/data.csv") if err != nil { panic(err) } t := table.New(os.Stdout) if err := t.LoadCSV(f, true); err != nil { panic(err) } t.Render() } ``` #### Output ``` ┌────┬────────────┬────────────────────────────────────────────┐ │ Id │ Date │ Message │ ├────┼────────────┼────────────────────────────────────────────┤ │ 1 │ 2022-05-12 │ Hello world! │ ├────┼────────────┼────────────────────────────────────────────┤ │ 2 │ 2022-05-12 │ These messages are loaded from a CSV file. │ ├────┼────────────┼────────────────────────────────────────────┤ │ 3 │ 2022-05-13 │ Incredible! │ └────┴────────────┴────────────────────────────────────────────┘ ``` ### Example: Markdown Format ```go package main import ( "os" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetDividers(table.MarkdownDividers) t.SetBorderTop(false) t.SetBorderBottom(false) t.SetRowLines(false) t.SetHeaders("ID", "Fruit", "Stock") t.AddRow("1", "Apple", "14") t.AddRow("2", "Banana", "88,041") t.AddRow("3", "Cherry", "342") t.AddRow("4", "Dragonfruit", "1") t.Render() } ``` #### Output ``` | ID | Fruit | Stock | |----|-------------|--------| | 1 | Apple | 14 | | 2 | Banana | 88,041 | | 3 | Cherry | 342 | | 4 | Dragonfruit | 1 | ``` ### Example: Header Colspans ```go package main import ( "os" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetHeaders("Namespace", "Resource", "Vulnerabilities", "Misconfigurations") t.AddHeaders("Namespace", "Resource", "Critical", "High", "Medium", "Low", "Unknown", "Critical", "High", "Medium", "Low", "Unknown") t.SetHeaderColSpans(0, 1, 1, 5, 5) t.SetAutoMergeHeaders(true) t.AddRow("default", "Deployment/app", "2", "5", "7", "8", "0", "0", "3", "5", "19", "0") t.AddRow("default", "Ingress/test", "-", "-", "-", "-", "-", "1", "0", "2", "17", "0") t.AddRow("default", "Service/test", "0", "0", "0", "1", "0", "3", "0", "4", "9", "0") t.Render() } ``` #### Output ``` ┌───────────┬────────────────┬──────────────────────────────────────────┬──────────────────────────────────────────┐ │ Namespace │ Resource │ Vulnerabilities │ Misconfigurations │ │ │ ├──────────┬──────┬────────┬─────┬─────────┼──────────┬──────┬────────┬─────┬─────────┤ │ │ │ Critical │ High │ Medium │ Low │ Unknown │ Critical │ High │ Medium │ Low │ Unknown │ ├───────────┼────────────────┼──────────┼──────┼────────┼─────┼─────────┼──────────┼──────┼────────┼─────┼─────────┤ │ default │ Deployment/app │ 2 │ 5 │ 7 │ 8 │ 0 │ 0 │ 3 │ 5 │ 19 │ 0 │ ├───────────┼────────────────┼──────────┼──────┼────────┼─────┼─────────┼──────────┼──────┼────────┼─────┼─────────┤ │ default │ Ingress/test │ - │ - │ - │ - │ - │ 1 │ 0 │ 2 │ 17 │ 0 │ ├───────────┼────────────────┼──────────┼──────┼────────┼─────┼─────────┼──────────┼──────┼────────┼─────┼─────────┤ │ default │ Service/test │ 0 │ 0 │ 0 │ 1 │ 0 │ 3 │ 0 │ 4 │ 9 │ 0 │ └───────────┴────────────────┴──────────┴──────┴────────┴─────┴─────────┴──────────┴──────┴────────┴─────┴─────────┘ ``` ## Example: Double-width Unicode ```go package main import ( "os" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetHeaders("A", "B", "C") t.AddRow("🔥 unicode 🔥 characters 🔥", "2", "3") t.AddRow("4", "5", "6") t.Render() } ``` #### Output ![a table with double-width runes](./_examples/95-double-width-unicode/screenshot.png) ## Example: ANSI Colours ```go package main import ( "os" "github.com/aquasecurity/table" "github.com/liamg/tml" ) func main() { t := table.New(os.Stdout) t.SetHeaders("ID", "Fruit", "Stock", "Description") t.SetHeaderStyle(table.StyleBold) t.SetLineStyle(table.StyleBlue) t.SetDividers(table.UnicodeRoundedDividers) t.AddRow("1", tml.Sprintf("Apple"), "14", tml.Sprintf("An apple is an edible fruit produced by an apple tree (Malus domestica). ")) t.AddRow("2", tml.Sprintf("Banana"), "88,041", "A banana is an elongated, edible fruit - botanically a berry.") t.AddRow("3", tml.Sprintf("Cherry"), "342", "A cherry is the fruit of many plants of the genus Prunus, and is a fleshy drupe (stone fruit). ") t.AddRow("4", tml.Sprintf("Dragonfruit"), "1", "A dragonfruit is the fruit of several different cactus species indigenous to the Americas.") t.Render() } ``` #### Output ![a colourful table](./_examples/99-ansi/screenshot.png) table-1.8.0/_examples/000077500000000000000000000000001427645071700146015ustar00rootroot00000000000000table-1.8.0/_examples/01-basic/000077500000000000000000000000001427645071700161005ustar00rootroot00000000000000table-1.8.0/_examples/01-basic/main.go000066400000000000000000000004471427645071700173600ustar00rootroot00000000000000package main import ( "os" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetHeaders("ID", "Fruit", "Stock") t.AddRow("1", "Apple", "14") t.AddRow("2", "Banana", "88,041") t.AddRow("3", "Cherry", "342") t.AddRow("4", "Dragonfruit", "1") t.Render() } table-1.8.0/_examples/02-no-row-lines/000077500000000000000000000000001427645071700173515ustar00rootroot00000000000000table-1.8.0/_examples/02-no-row-lines/main.go000066400000000000000000000004751427645071700206320ustar00rootroot00000000000000package main import ( "os" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetRowLines(false) t.SetHeaders("ID", "Fruit", "Stock") t.AddRow("1", "Apple", "14") t.AddRow("2", "Banana", "88,041") t.AddRow("3", "Cherry", "342") t.AddRow("4", "Dragonfruit", "1") t.Render() } table-1.8.0/_examples/03-no-borders/000077500000000000000000000000001427645071700170735ustar00rootroot00000000000000table-1.8.0/_examples/03-no-borders/main.go000066400000000000000000000004741427645071700203530ustar00rootroot00000000000000package main import ( "os" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetBorders(false) t.SetHeaders("ID", "Fruit", "Stock") t.AddRow("1", "Apple", "14") t.AddRow("2", "Banana", "88,041") t.AddRow("3", "Cherry", "342") t.AddRow("4", "Dragonfruit", "1") t.Render() } table-1.8.0/_examples/04-no-borders-or-row-lines/000077500000000000000000000000001427645071700214275ustar00rootroot00000000000000table-1.8.0/_examples/04-no-borders-or-row-lines/main.go000066400000000000000000000005221427645071700227010ustar00rootroot00000000000000package main import ( "os" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetRowLines(false) t.SetBorders(false) t.SetHeaders("ID", "Fruit", "Stock") t.AddRow("1", "Apple", "14") t.AddRow("2", "Banana", "88,041") t.AddRow("3", "Cherry", "342") t.AddRow("4", "Dragonfruit", "1") t.Render() } table-1.8.0/_examples/05-specific-borders/000077500000000000000000000000001427645071700202465ustar00rootroot00000000000000table-1.8.0/_examples/05-specific-borders/main.go000066400000000000000000000006351427645071700215250ustar00rootroot00000000000000package main import ( "os" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetRowLines(false) t.SetBorderLeft(true) t.SetBorderRight(false) t.SetBorderTop(true) t.SetBorderBottom(false) t.SetHeaders("ID", "Fruit", "Stock") t.AddRow("1", "Apple", "14") t.AddRow("2", "Banana", "88,041") t.AddRow("3", "Cherry", "342") t.AddRow("4", "Dragonfruit", "1") t.Render() } table-1.8.0/_examples/06-footers/000077500000000000000000000000001427645071700165055ustar00rootroot00000000000000table-1.8.0/_examples/06-footers/main.go000066400000000000000000000005151427645071700177610ustar00rootroot00000000000000package main import ( "os" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetHeaders("ID", "Fruit", "Stock") t.SetFooters("ID", "Fruit", "Stock") t.AddRow("1", "Apple", "14") t.AddRow("2", "Banana", "88,041") t.AddRow("3", "Cherry", "342") t.AddRow("4", "Dragonfruit", "1") t.Render() } table-1.8.0/_examples/07-padding/000077500000000000000000000000001427645071700164335ustar00rootroot00000000000000table-1.8.0/_examples/07-padding/main.go000066400000000000000000000004701427645071700177070ustar00rootroot00000000000000package main import ( "os" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetPadding(5) t.SetHeaders("ID", "Fruit", "Stock") t.AddRow("1", "Apple", "14") t.AddRow("2", "Banana", "88,041") t.AddRow("3", "Cherry", "342") t.AddRow("4", "Dragonfruit", "1") t.Render() } table-1.8.0/_examples/08-alignment/000077500000000000000000000000001427645071700170045ustar00rootroot00000000000000table-1.8.0/_examples/08-alignment/main.go000066400000000000000000000005551427645071700202640ustar00rootroot00000000000000package main import ( "os" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetAlignment(table.AlignLeft, table.AlignCenter, table.AlignRight) t.SetHeaders("ID", "Fruit", "Stock") t.AddRow("1", "Apple", "14") t.AddRow("2", "Banana", "88,041") t.AddRow("3", "Cherry", "342") t.AddRow("4", "Dragonfruit", "1") t.Render() } table-1.8.0/_examples/09-rounded-corners/000077500000000000000000000000001427645071700201405ustar00rootroot00000000000000table-1.8.0/_examples/09-rounded-corners/main.go000066400000000000000000000005241427645071700214140ustar00rootroot00000000000000package main import ( "os" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetDividers(table.UnicodeRoundedDividers) t.SetHeaders("ID", "Fruit", "Stock") t.AddRow("1", "Apple", "14") t.AddRow("2", "Banana", "88,041") t.AddRow("3", "Cherry", "342") t.AddRow("4", "Dragonfruit", "1") t.Render() } table-1.8.0/_examples/10-custom-dividers/000077500000000000000000000000001427645071700201405ustar00rootroot00000000000000table-1.8.0/_examples/10-custom-dividers/main.go000066400000000000000000000007161427645071700214170ustar00rootroot00000000000000package main import ( "os" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetDividers(table.Dividers{ ALL: "@", NES: "@", NSW: "@", NEW: "@", ESW: "@", NE: "@", NW: "@", SW: "@", ES: "@", EW: "~", NS: "!", }) t.SetHeaders("ID", "Fruit", "Stock") t.AddRow("1", "Apple", "14") t.AddRow("2", "Banana", "88,041") t.AddRow("3", "Cherry", "342") t.AddRow("4", "Dragonfruit", "1") t.Render() } table-1.8.0/_examples/11-auto-merge-cells/000077500000000000000000000000001427645071700201655ustar00rootroot00000000000000table-1.8.0/_examples/11-auto-merge-cells/main.go000066400000000000000000000007371427645071700214470ustar00rootroot00000000000000package main import ( "os" "time" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetAutoMerge(true) t.SetHeaders("System", "Status", "Last Check") t.AddRow("Life Support", "OK", time.Now().Format(time.Stamp)) t.AddRow("Nuclear Generator", "OK", time.Now().Add(-time.Minute).Format(time.Stamp)) t.AddRow("Weapons Systems", "FAIL", time.Now().Format(time.Stamp)) t.AddRow("Shields", "OK", time.Now().Format(time.Stamp)) t.Render() } table-1.8.0/_examples/12-load-data-from-csv/000077500000000000000000000000001427645071700204015ustar00rootroot00000000000000table-1.8.0/_examples/12-load-data-from-csv/data.csv000066400000000000000000000002001427645071700220170ustar00rootroot00000000000000Id,Date,Message 1,2022-05-12,"Hello world!" 2,2022-05-12,"These messages are loaded from a CSV file." 3,2022-05-13,"Incredible!"table-1.8.0/_examples/12-load-data-from-csv/main.go000066400000000000000000000004311427645071700216520ustar00rootroot00000000000000package main import ( "os" "github.com/aquasecurity/table" ) func main() { f, err := os.Open("./_examples/12-load-data-from-csv/data.csv") if err != nil { panic(err) } t := table.New(os.Stdout) if err := t.LoadCSV(f, true); err != nil { panic(err) } t.Render() } table-1.8.0/_examples/13-markdown-format/000077500000000000000000000000001427645071700201325ustar00rootroot00000000000000table-1.8.0/_examples/13-markdown-format/main.go000066400000000000000000000006251427645071700214100ustar00rootroot00000000000000package main import ( "os" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetDividers(table.MarkdownDividers) t.SetBorderTop(false) t.SetBorderBottom(false) t.SetRowLines(false) t.SetHeaders("ID", "Fruit", "Stock") t.AddRow("1", "Apple", "14") t.AddRow("2", "Banana", "88,041") t.AddRow("3", "Cherry", "342") t.AddRow("4", "Dragonfruit", "1") t.Render() } table-1.8.0/_examples/14-header-colspans/000077500000000000000000000000001427645071700200735ustar00rootroot00000000000000table-1.8.0/_examples/14-header-colspans/main.go000066400000000000000000000012311427645071700213430ustar00rootroot00000000000000package main import ( "os" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetHeaders("Namespace", "Resource", "Vulnerabilities", "Misconfigurations") t.AddHeaders("Namespace", "Resource", "Critical", "High", "Medium", "Low", "Unknown", "Critical", "High", "Medium", "Low", "Unknown") t.SetHeaderColSpans(0, 1, 1, 5, 5) t.SetAutoMergeHeaders(true) t.AddRow("default", "Deployment/app", "2", "5", "7", "8", "0", "0", "3", "5", "19", "0") t.AddRow("default", "Ingress/test", "-", "-", "-", "-", "-", "1", "0", "2", "17", "0") t.AddRow("default", "Service/test", "0", "0", "0", "1", "0", "3", "0", "4", "9", "0") t.Render() } table-1.8.0/_examples/15-only-wrap-when-needed/000077500000000000000000000000001427645071700211355ustar00rootroot00000000000000table-1.8.0/_examples/15-only-wrap-when-needed/main.go000066400000000000000000000005621427645071700224130ustar00rootroot00000000000000package main import ( "os" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetHeaders("ID", "Fruit", "Stock") t.AddRow("1", "01234567890123456789012345678901234567890123456789012345678901234567890123456789", "14") t.AddRow("2", "Banana", "88,041") t.AddRow("3", "Cherry", "342") t.AddRow("4", "Dragonfruit", "1") t.Render() } table-1.8.0/_examples/95-double-width-unicode/000077500000000000000000000000001427645071700210475ustar00rootroot00000000000000table-1.8.0/_examples/95-double-width-unicode/main.go000066400000000000000000000003651427645071700223260ustar00rootroot00000000000000package main // noreadme import ( "os" "github.com/aquasecurity/table" ) func main() { t := table.New(os.Stdout) t.SetHeaders("A", "B", "C") t.AddRow("🔥 unicode 🔥 characters 🔥", "2", "3") t.AddRow("4", "5", "6") t.Render() } table-1.8.0/_examples/95-double-width-unicode/screenshot.png000066400000000000000000000137571427645071700237470ustar00rootroot00000000000000PNG  IHDRR`j; pHYs+IDATxy\gL@,p%l UYԵVD={Zֽj{mmE,uU) P?I5YG}Hf}/$!!\ B&XdtU?Bӌ7Xwr.y ]m| #-`!h!D F!Z0BB0Ba #-`!h!D F!Z}tر 6n\]] 'ٰq]hhUտ{no-˝̙5d?}(GFƶwvv uOd]gz$tRR¦W7 2XsfϞ=jxd;n I=;QSs#_o?|YJĈ"6glyKeeGFAI/aC]#@(vtCS$ɮݦ$ b= F謭JT!PȨ-7jjjU3*(^_qrdHW9DSR'/L[P^~ҡ-2455ٱ&3zxyy:~[ݒe˖ v 9Ɉ?vww,G%N;0zDEE,""{ڵke0lΎ עJ.cǚFȨx^ebB&JO<Fz}`@;0~Ŝ ))#$AFsʕXG7Tq2!qAA׮٥ |i^T_Aa pu/hnnqtC'N(j|Dwww }/L2^{+W LVWW^!g0 u&3`@#c|ތhmGwQXXT*!-_j(at R{O7 aa#rАKz\^SS'J˜NK5cǎ3u0{۷}FYT;o\znn^nn]J9#-`!h!D F!Z0BB0Ba #-`!h!D F!Z0Bl?Ӆ]A=O3"!ڟǠ 5Ɨi!Z0BB0Ba #-`!h!D  `1P/8v^nz==kZzHn#9h[[y`'` t\[6Imd;nG`l݅0bML;ڡ8t$CCCxb˵Wð;cO`u Q28*o7͓N}Xz]b5|3kfCɤy%I^Z9h.{zCG ,_nב5kWbjk222L˳QQ|@]vee妹cŌ<8D p\Bq5ӫֆKf6og󽊌;oNPPIr~j''' Md.(-$L%H d=?w%CC$,,**R*ook!4p!6=@$Z4խUU ,lĆӶ6-zdnի@$}?]_:fϙuOad4F۷0O?dL|1A ֭_7&FPWWcY3˚ŋ=]zIqn //ӦVU]eS L]vRnG|4aYA2 lVk2ғ󯈞~<'-U^VnL"t"::ḹqFT*Ue٣F(Boo7þ\]=xp(yG./W }s8WQQIQH$|u=j6s{[;̞3[}OڽnV^b1w D"4>ߚ7KDAq`HbǏ3s˻-#FN&iʔӧO0@== H̲ԫl<_5QbK~DXp~G\.wUv)稯oo8‚—WmuFB`i 0I!LaH(P 5n]R5 `07̣( ¢k^ f[Oh0w;$ b޹sPԖt 3GɼӦ.^^Uu董==K.eWi)jO?2q|¸uIw~ٟr6\BCC{2bT:8 #mCxCn)e2tZq%;Z斀z> {zA\kk^ϽȺcF  -.ZY$.Z+Z#E2MYY̙5wޜsfel4=?3p6cz}!`牢; e7.~0cXz`(06`CCčW2 g]ƾ-jMN΅pIXp$\sA֨⒰FޜZ.,,K#F)Nd2e-UggW633Ak7B+8bB$MstKXIVϑL(3#u`B37 ݼ`X = : 0jsQx!… ,I1bxkkޯrjrr"uBpo"""/_usxzn:3rՅE@@LȞY53;lЕ+_*-)mqC}NC^s&Q* p7//tH__~%{!%_ZT{=m\֒^IǍyƒ26I[ЖqGM*/d2cF@al1>aiuf4MTT'~8?X,A/%0Lv>-gjG` x0@08 Eg nKhFko.^EdO.*}/"6nR*))1U޲RǍ3 8}Mii #"}Ymm7?qܲ~ P*5K]Z^Yn޼11у%vww77t[vڡ#if};y^{tB)l6ի՗^iŲ*77ȑ'LHV(wmꚐVmlmǟYݷ-D{L&st]*H<>QQӦO]vCWc_Tʪ?bT[[ۅ)\cMr\.Hd2"KVkl)uɑG'Olҙ3gݻϡxn _~x… ;?`TׄѰaC|~JĔݷ#R!dӧ,ZpnHx<]RUWWhuAkoo'=2B:ATXZZZB=tT:.YWß)9!4bcG{zz:RI$1q3g ``HH/d[]rٳgƍUVDV(ǏwtO2yҧh5ĤF{V0BVWW?l<O%9tHssU*Un޺h„6rތ̦& 9^} "˓I]t[kۻoj"!Z0BB0Ba #-`!h!D F!Z0BB0Ba" (t'k z-fqK!4-`!h!D $y<IENDB`table-1.8.0/_examples/99-ansi/000077500000000000000000000000001427645071700157725ustar00rootroot00000000000000table-1.8.0/_examples/99-ansi/main.go000066400000000000000000000016531427645071700172520ustar00rootroot00000000000000package main // noreadme import ( "os" "github.com/aquasecurity/table" "github.com/liamg/tml" ) func main() { t := table.New(os.Stdout) t.SetHeaders("ID", "Fruit", "Stock", "Description") t.SetHeaderStyle(table.StyleBold) t.SetLineStyle(table.StyleBlue) t.SetDividers(table.UnicodeRoundedDividers) t.AddRow("1", tml.Sprintf("Apple"), "14", tml.Sprintf("An apple is an edible fruit produced by an apple tree (Malus domestica). ")) t.AddRow("2", tml.Sprintf("Banana"), "88,041", "A banana is an elongated, edible fruit - botanically a berry.") t.AddRow("3", tml.Sprintf("Cherry"), "342", "A cherry is the fruit of many plants of the genus Prunus, and is a fleshy drupe (stone fruit). ") t.AddRow("4", tml.Sprintf("Dragonfruit"), "1", "A dragonfruit is the fruit of several different cactus species indigenous to the Americas.") t.Render() } table-1.8.0/_examples/99-ansi/screenshot.png000066400000000000000000001060721427645071700206630ustar00rootroot00000000000000PNG  IHDR<+ pHYs+ IDATxw\SWCp0!e^(ZghU[[ַ}۷HuoAEAV"=!!ǵ)r !'{ɓBXZB!hB!j&m!Bj6B!5IB! !BH `҆B!0iC!R}Z:-\^B!*K Og]Dˇ.ۼ7#.v. 6ݳ~iw-7k6!PlȖOO.ۼ7ɝnB!z{RYyo\v. !Bd_I*B! _`=B!0iC!R!BLB!&m!Bj6B!5IB! !BH `҆N(BnI[W0۶X?;xhԩS;?T>P>JyaywK.o)ׯݸA:[}oeշ_~522J:=yyO<={X,UegCw9,FbpPBz哶2bK  `G,aoogoogmc}QUuuupaUUU|>Y]P-88hfpH$Fkk+m-d  #A =O{C?a ]^7qo5{ʔIEu(.t'NRUm999|j 4y"u# RiwPXloPj)I"TUV=<KV <܌fQYYy¥._ȳZZvoӤ?cY Gp"СM9@'om׵J)L&b@f rnnnnn.(.O1L477{4DɊ =ӦM2РAR\\tf~6|{e"`=NĩaO$g8iT*}oHޮ'o+*,79=uuulFvdii)ץqc`W7W}}=mmmPpБ UkzNLHqǍ;`^Vg8pY3Nӣ>$@W,HvS$6VqܝhZZj"vM0~Pr Nvrr\K@`bjq.++۷ƿVXms.fl5`|Y b12הzx\,"ʊbד7Ql6$ z:Bq]vQ#\Mu|"uz%%%@&N&OOK߹禥F^VVE=qq,2՟|L^¯.ok~f- 7oݻOhNȜp,,,Vhaa'-[r*;YX,IK.x v&#X9[p/]zܸǎ 6>ٱcT*522p^gizby#̙?Ϟ8qjAWo?q҄'NQ z*7;w!%K999 UvY<ڤV;XL}}=OEqtZZzFFEEaKKKO#"ΐ_}7bg,S*;&Ԧ=|܆iiS> $/7 #G|ӎ d#c e%v%ȮJ zRT*&'<}R@1n#ihhll]JadlܲH>TTVvTψ3INZVH1ntp"v?Рij2߮nܸEyr۱ca999 GuyԮ]G4h{ A? ߩ`!sftT*555@,߼zk׉N8~‹.}f=Ñ7osPVVo߁?|„b="Ӵ:=r|P''ǭ۾Θ1 555 D/@]Oy#^E"իԃ^R*'mMo&]`Yff C dg\t9!!Q%XG\M)#4+&M߿ŪyGn޼5|pKK MM͚ӧϐ:Spk rss .7''𡣲 V+7UK+ًeYQaBp&#_pn\}Hp#BLB!> !*bmwЛ !BLB!&m!Bj6B!5IB! !BH `҆B!0iC!R!B?ߍ B`|jZ]=2p&#R͎QB!5IB! !BH `҆B!0iC!R!BLB!&m!Bj6B!5IB! !BH `҆- b DRpt岰tt;h{h߮xRܱeI+x~VaƆW]&LpтU>(+-Zm!+jYCg^r9iiK,+Wzmf',:c(f'_t-^?*?ѵ ̙v⨖U)mݛ :Zv|ɝ<:fhhpaCU҆0=}fz*i!Fvƌ vv))ο<~ FFF*333s}oo%=ݻ6vM4޽*YzctR!ao:xYш/  || !SQHȑ#O}€e=J*z 7`z6;\vTzzzNnޖJFp7"#; !Wa=] :.mds⤉<FUVTܽ{/<> txqff_ttnWs:xf{|~tUqܐ۷n+n EZ]VԝM^Cz|GϏ 3110qMIRBpo8ݻ ,--JK@@YܰaC M2hqqmV>OQa7;nC,wzѣG-FQTT6[ UWTWWkjj*E=zLSSs%M\}}ccܴB{;H*72NIy}tʔIuskiiX[*AݣZZzW.90ŘL@(Eopp2HN*.'dCYpۖGbj^40!^l9!uuu;ֶ;w<<%0ݍL@(8C1r}W T*͛b e]r-/?֬''Pɳx’ DPPP88Rhy$x&ERt׮=P],YH͛#GsClTVV6^Rя(MJ@cccqqqtt+''*rR]m.Kj Bof3yjAaah|w!OPl r6~*R:5yѣG98Q,'۾]} }PcS,K0:7P(lY4|GUWW }QF;/Y1m;C#Gp/).))yE@J1#55CI4ؐF# _0ٰw޺.n*12fL>N2x+0ӧO W\@TECC877Wޅ|~^uN2駝ۏ=xީS&@i忮PvɘLMM޹+Jt:N'G<1 md2Q&+)ڥohhwqjGratd="ܺy ]+QpӖ[w#'d2>߮ {มIJ:-m /VV}555I|~fff_477ٕNk^5{6;;G6 [;ۦ7;UW/_z5p5ZG9 yy5 $zZji5Ϳ ͖Pp80e)4Mh,,kN&.M&GSSSѭ'sus/_"Dq::#@>m_3CBfݸ~C ^c|| Z+.]E2g 5t \$'|aY3W|E7o>yThRɊCv@UUU{ՌP(ѦZQz=ϤP@y\WJLƿե@,GE>9dtÇYj|*XVקP(|y>}~[L֯MȡC 8{̓ JINiz}j.h377NNfR{;^>tD(1#pʔIǏݸqK[IA[0G' `04jvnYD^a=55b~%¦Vqtr`XCC&.X0oQ~ՍuA΃kii?G3 <(*ruz{yfff޽|ihdHFd0>eQcc#lJuu5\pGMUzhHS!3֭ut|}M6o"(EkWSl6`TVTz۫N[[ H$~(SUU zMX,r3(% b1ʙvE4W^9jy|X5vUؽF:Ų44(Fii7⒒*H$Iܳ8f099/}|r]=]ޕ+We2L3sw6в%dg>f3MMI^t /eo/MKKUI%GyLy>ɰe6|$%ue1++^dEzgN74}iכd/>K.v8\ΩS)ArIР9Ze_?>,n VWDD3QߟxA. }|/(uH@ܳN\UXP8|0S]55.v`ZZzӢvE49|~ĉKJJ[=׌j|*q]zS*癁Ӹ\n OCCim΁Kza=yp<5Կ_?x-;PT'h $rrr***Ο1ظ6  ^/OIN/syeu/2+_p4=_j`Ϊ-l2+Ry|}}fRbIjrYw?pя++Y>,64(:@KO+- WGNΝM1x | *88h+%΃7-rk~hdd8vl@ffm@JJT*]x ?}kddTyy9b S$d~rcc% ޹o8gn /==B3vUeUMn/T*]R#|?|{ν:[vɚ2u^dgg;jĤPm۷N6eϯ_!ސ IDAT` !1gWFFF "y;[8vo6nx=#BѮG~QY FFOPwgϯ]垿uQSmož3nQJ㠠1>cl6[DہBٳƭ|wyddiaH^}137MOx" ޽V}N4A6>-*,7nlYiYJOU>ih廫Vo#4v ;{ҒNh@}NM_ 4 <,EvjJ8`\ڴ2H/qˣSdhTWQψocqqqeɎy:>zæAǏ`0*RSQRR Y:0/7IK-fdd?vn(d˖3gΘ1# :&&VXAVەbna8b={>z+??~Xpܷ@eEelLlvN8z{{kj 󑺳*>>a۶f 3[ \|5,$YDQ(U_f@WQG⢢$XhU Yfff(cyu7(tYYIIAA3"d]\f 0 E6Oទ 3¢3𷵳'ߜ4y;,xmЅ󵴴dWL0fWVV%%&>!(fK+ًeYTTHUa,[bkG\GQԛЕzl?}G'ۮ"T ;[K cQQ=ݱ4FK}Z~esZjږ-[;x+qh!@!:Wp̹)$']{?C:]7" B]Ą7o\ )?_@!Eݽ{OC6}4MM.USI۞?^\EQPOU!&t;Epܐ֭;G{xz!Bo7 cLB!&m)`tjN3!&m)k„kw~iC-q׭5BIBJb0M͙3; tccc^~uUQLMMZbBB !4qk333ϿV:277_tJ*D!Lz\~#2RQPB|ŋyz0` E!.osR9_?S jq.fkYSSĉSFソʪomm; пDgʹ(7y2y_^͂742ܼs,KKuFG?ypH,6114iW_~tvHh}! !2;;;gȬztAlnhhCU1Ocf$%ܹ7t% KJJ7F$@AAGk`/JwS__,$>466GG?iZI Lx-_0ݴ۔ԖW._%(JE"ѼysY,y,P&5%WJBB״!}}}CCȨǙLMM@flp-pvmICS"141ᙚ޽sW*t:Nfcؘ؆ld2;z&AnC\< ZFI-e2G {Qccc龾ffff'OBuLRTt-[Μ9cƌ@ꘘXam1k̐9WNYal쳯7l4} *55,srrtrrb Ǐ'Or55IdYQII37dAb~ ֬^2jHX\TT#b/oQF46Jkj::glq_͚aÇ|BV66ݳ~i7FӣzMV5_|C B">HB> 55 Bj!IMM۱}X,XBWä !D%AOW!Pä !D! > !BH `҆B!0iC!R!BLB!&m!Bj6B!5IB!h B!j㛖ufiL/\!Eixz!BH `҆B!0iC!R!BLB!&m!Bj6B!5IB! !BH `҆B!0iC!R!X,VwѺhVpYر;!j?RvbDI~VaƆ?,~UH, -<<{F>#(ٌ  6T g̘dvv))]ngZh022@&NpBJtNZ^ޞ|~]{TRuzRm):@+ՑICGr_rmmF k>[Y7Ƥ 0x?GeddT\\LQDQԩL5_&>uK.־>rfhd|;ׯ;.@6ȑ#V*6k'6oݼ=jސ@go@=\:::q *Orsse766")7=_zZqutA]Ý%V@~ӽ,yZ>Eeuף~;ůo茹T"Hv~ 4hPQ>$-- 22j9X6Mڲsr qɓ i>|A/\46t!e0|nk; ÇVVVE__ge՗f=r,;.ۋX̲ǏO (P_9qDG*+*޽h ]𬙶+re}-kjj"8q\u.(//zG~׮۾ d=۶ng)FF%ZWbYc+:c 1`Ɠ^֋%}MC' 6әx8 9RY#v:i.s;c7UB|YƍzN;. ## HMMM##Ǐ 9:#99(XXI}Αrr:6tGN˼ܼG/Y&33͹+WNG`ff d"dҤff_}58:8XZZ=sF`fzodŽ;iǫYѐ!n+]~䥋IAjddT^5/5kyy165=p5_Q__;t'~0 [XoT*eϳOF1tɤM^?3?Ϥko^~@_Gu܊XEʎDs^0ϝRXXxҲ2x%k]۫3[]:B%/ahdy7YYY Đ"OccgqÆ e04E`ooGۜWa|*Hh4ڊ 8~)as'YW z䓷k5ubm:lO^,錹H]^tF6}f ywd6SU]=wnHֳ+zY(;v{à^됔Q^v,Ւ)(uJ_kcy\.wo[D!ѣGrvq&X,N'9j:rr77;v$ϙ AB' ܷk; F';ڵ몺 լc7 M-0L]].H,V,ue]_A q}䩼wU;<1|8مdy[u(i#bc4˖-v"e˖y&EjjRTKKd;>5nSNt:>}hjj*?@ xDq::5W" uu`FP`HȬo}`.%'|aY3W|E7o>yTrp xY3NѡKʛ\SW'rahk@iiiۥHPUUE1{tvl%AxPU$|nWAERd2555KJT穉ȇc<0L:ncc}Б6Ry=\ӽ=?snK/ #6LJ,߸u6ijnK}z%2mH$OffWDD~ldSWN,+4t~h|YFİ4 _O4xPTCa <333wKxYCWEjHb|uv]=]?_iӧ:::|n=.=|!766@ӧaUUU^,KQ!/\p8.D7('?J (i.K^hi~=W5R:mm-7PjSrrΝ?5/I颭F\(bϸ͢W5TVUQɦhhhYgddcbbƎ 037#o,DI4훯7O/]Νp9NNJL"5Jƍ g )Ar.RuM ,(8#g£/#aTPPPTXd`vRޥo+/|~~~^nޘ1ϝ@~69ɓ/F~ڷٮ>('^eQwRӎMj^*TXP8|0SywAQjSIIR CEhv7Jq n.ۻ͢>?y%%ii銬O)=I[ #&xWYjz1C) "_JGEt֯m+Rk7ߜ:mP(LJL6Y3"k_|ի׬<,,-ݽ^TNΝ%MnLKO BdM:YT/5zdbbysQA+](iti355K7wϾk߁5V u2j"7FdjOEbt/ #-PQ# ں3+Rk'NŞ^>UUUiiaO?!EQD*,(wט&())- }kNMMɓR޻@h廫Vo{'&e89ވC IDAT9Q|~i~~SR-XgG`lHi￿jےo+e9;B*`0KCJ5EHq.~aԈ^EEMCc@F<bdl7 5j$x-?0iC]j7fdd7!.]=]  xnw"11͛? d~?W P6 :*:ODgDw%܉Л$++{w; 5ЋnE!R_!BLj! 0iCHQ˗?ۻ; B&m)d;z!P !BH `҆B!9mXs[ہA>tTvf:\QQSMhnah-[~˗WTPP{Y/6oBbuCwKBu;Ljc~nT*uvf99/>fN30[+Ds޾u x<^?1<<:[-z8zQ.яX,N'wVBxxMBJzp?\\MLxwܕJt:N'%`@~JW4vKG'G>?BNx !% zm fϚ :::JW."d2tCG:0B!IBJb .^ᣦoݣW5?Hm!&m)iܸ{_PPPTXd`vR,>?y%%ii%%MKmmmWx;)w~U-!POIBdL,َ=211)&&V*?qȨr6f;ŝ={~/,--۬pPB7&m)*9%<0p*/+?{|?R)>zæAǏ`0*RS:ĸgqϟg=xլGÆ!P/IBz+fddmGEo] o."`0εkׅBaKަ!6z&9jHguwD!&m8&&yO !Pä '++{w; B= "B! !BH `҆B!0iC!R!BLB!&m!Bj6B!5IB! ,ld/mۍ B_*X5-.6 aBp&#R͎QB!5IB! !BH `҆B!0iC!R!BLB!&m!Bj6B!5IB! !BH I6! DE[[CApmXbG *(j1Ebz4JK.]RT)gi1BKF,N*/?"FS ?ؽg͜+̙3<<9\NIIw.^$HJ^V}: u+U;g쒒o4Ik#((pFPRS9\EGj:B A#݇xyyZy m=Ypth4ZEEE||⩓JJJ;.|ת[+**z/Z*H]jyLIɍ<:}vVuMGҫ(F>#(U8p[wKJ q{9A=skc1cFmJJjw233W[[ &Np}UFۓO޽kJj^*oj ?###V{TWFؓx<&Aab7~?[SS Q>i|~$+~eȑ#>i377|q4= .j._k7Ǝ M<|訠V@~]rmmFTMOO>ޣF ItN rmR.2t\./-w /9]wxL8*1ncnΝ4i"ώw)Ĥ;p1/6hT^Q% r&פ$e=+ Çf})22"99#K-1cg 11).6NPM:~_ o8O6Mڸ;;w^& vq a益!uUQUX}|WYUPPpwQ#H$RGe7-:h۴yàA$#v0344d0-čGsTg S$qs-JysB>L7nW$pW(Y,%%ώz+IPw "3SNĤSigƺ2Qduipт͛nD- -Z0|G G33w9jAUT7A\.??b:d2T޻wBWmT~+.) 噞UER$^N:OE7nجZfR-E/@`b644ۯ ~|zj{.u?v߼y;((oGH&W*u9i``nddf;o$2 FFUp#I,/468{ZZdTT-W 0k Lvoږ"bŊCÿ˄e]kx<򢢢C}!b0G~a2\;;}$ UAߠ|D2fhGww Ku'%9q-ƴѦSƩrmxMML##P3mou짨#QgL ,,-@O_ۜFhTN)m`s/jW^SSӃ7lXgjbsn= tvݮ=m/ _gK}֝ӧd\f-]q'cfRYCCCG CC""o޸u$?/0O04lA}=UXr:#B 4h:XTVj8p8\ l^͇3T*JZ(,I:|ufgەF2J՝R 9\/ _@~)}R~~vJ%DB@ uٲXE,Z@=uƍ]v=̞>ջn'??sHmKd2BѦ?0A+?'$jP6mׯ6!8wU/ P65g^;v[9kۗ6Խ6[lh#JMMM(fЖ mjkYOb25mEL̽ͿP:0KQ1lLR //t. mv=RWښM_mQobjjg?_ +9s\fF }ˋFdZjPo7_lSiiiz|jAA@5e䊊hYW_VVVUUiVϟ W(, ώW\TfuIFX*=z]iE6ڵ]%%%E~~.]j #:ߐ@08h~,--DҎʼUvJX;-z1hDm@ ^Ao)mYŀN;ٕ e˖(ʢbCCA3:qjg-j^˳]`s*&12JAEHYO~olIiUCOCã:jsÇ9t4",++((njRy~1ލhv7SNNeW6܏/(hlly(oM$|էصxHfMOvp+.^v^g"KT*,]s72=-===c^d/''ZTm18qBmM1=#3((%~/IOHO6seC;wbf̜f7nd CxAc6hl9vM7&'x ASۼ7+P6)<1HFrk~acc=qbp^^^ğ<&I%cc㬬RS232;ӫS755]pi>LLTwU>(ګͣRڨ~HrAA;T%InK~^RQ[Sׄ}= R*,[V?.dۗh[tu!>c*uڤ'b\X|'O:w|B;vRI YR[Qmd2aXX(I 5  hт2U;ӫS_tT*Ο)D.QH@á#gj{R὘X(Y[ذQa`l`8߸Rat;Eg }E=|WЫ wqq=F?mu)1+i;BCCfܼy;Sۯ_?BE0Çe-FFFz)G3BCCBW[lͭqJn.k|>%^Dl!CH"to A}zqM7pww vƭ訣/5e[v^T:X7h6Y ڵuV/]?K$L=wxBƍ}S̬X{P-~{QYYYtt;Zl=:~/~ c04ӆP;߰ɩ?͖J)?;QSS'<2=|xنRg̘ncc]YQ)HtYG)FSNx :&ٳiOaί c?.//^upX7ƴԴl;xu&((p괷m^ mCbbR\l\:u/w77GGNj.׋p6m?/ɓ7nܴ ˊv-K IDATA0F h1iذ+V{竿@؈t{)DGe~@u?ֶ\ǎgYBfv6,U;g'{knF$Eqm^ mõ_A$I#Y,T*L~ {{566FD-((ܺeꇤ.22=JJ$W7W$;&xdf @T'&&鸢%KWTTnܰI.@iO>JIN^"8qJ6"Iqzy{%>LwB%Ȣ 63Kes2po%%s<<)6@gEEd-S$b,\d󦭪[/KE >,)h ^m֍X>y:ax/~}"8 v6}:Sԑ3Ossֳ YVOm۷=܍!IP;h >''4s QD"3zj@jaho_é7˭{&66N۷ngU#4^ΨӆfDD߼q+Htcc_` `hxA}1rVade=j9+V._[>uuW ;@X9Ϳ733kWדYjQBglllhhOk"fǏz\xy//@;w̘9}U7nܔ h>YEzzĉyyyIHdj< wͿo3QAAo/YT{MzzFzzƑ#k׭޴ycB|Ǜ86ێOuzAKdƣnjJHxU?gU݊^hCHWΞoT(cǎQ*: K*o1gٳCjk뒓Sdlr}"#._^V.@qQ [,8~5 MBCas-ZPVV0%W5yD&Y[Sij~z>N},!.N|IIɶm,^0/&%9`2LfhhJdBcTL-? 3#34tv]k3QO1r|T);wω7W,_ɓیv4Py6o\&_Z1!ѣ+ptrVփ6,hzT0&^L&st[=Jmߚ4{1z||WYuGDww,S)ӆьzxRwGg/;ɧvw8>Ox=2a_MF![Mˣ!B6B!zl!BhC!цB! ` !BFB!P/6B^m!B6B!zl!BhC1LԴC@˨|a<$ cڸpL8⚼gG9H4cU J]o5y {êʪO B\NMu{) z}DqF=TQ͊}z/JKMOU7]^uu5BZ;"^zQOmܰ9u/Y1f>ג.AS|5fj\ڽ\S6ʡ#-+sk [016״:?*65Cl^dDHeK%]5~<|HlLc&AX yvӧ._~ٰi/ys+**oܸ^ o~ ݿ1|څ8w E{Hz^mzu=mcƌrq.xc5f sO+mF{xzQVXZp1iu P@/+fmmhQFvz?q`ee5x˓'O; YKiyyyH$XSu^ FRo=Ç`cc2w E{HzpaXomcݿ;%% ?߿ʒӗ1q}MY* My$Q~SRwc];$I蓙YIq q=KA(v E1Hwjݿ;'+& ay I~&jgeo6#V\UOc1Y2,Y=W'U1Cژ25ՏS]RMZxc_~Z}k+k*)"|?#s3ޤf~wuu2?Zky˰k3=ǏyiS{=ѣ|-vԨGrt|Pml++*Œ?]D1sq8f ZT}7%~:'l6[*&'tDM>>aNNNAD{W"& ߱>~©Sg``>?XUUϜ>cNN`BpЄ *>xr׮[meegU rUU g\ρiA.5jN5l~R4* I*+k+Ss4\I6i`cnzRBajj:°CmADD-((ܺel[[W| nGX,Lp6r`nE.ZPR\yVkBE d2Ts޻wBR,//OLLjy <;^Im$oug.ÐH$'NRbÇH\tqEE T/-~ɇ^^)=?Ն^;"II2F r;ZT&%>^JUl"T4G},)m2R-\0nwC{eb)WW1mf͐dׯh$ub5ač˶=~CI1XY?$ZlZw}O ym0, o򢢢6 [ZZZ[[ݿOѴSnױD1}x}:)7goookk~[۳uAE{iRSmollsrrL#9Ύgoos7$ICCCCCCo̠o}]]FGv uqL^SSӑkFFFKaC6D}QLs֩Qc|=a2Q"l3u\&W4Z,oà#=3bŋU IffuuZm3h3;4$""[QG@ek Je \9[vfe=j9+V._[>uF&|X l 焅i>3tEzUmtmp8\ОÌ|iZgccc## ̓yezhq}ih0w7hтE\pqc׭] ϋ۸ >zXS3T-9V@ SKc!"=R MJ%Hh1I#4119|Ow?AR`ee?hmc"u`Ѭ#b1̚F #JMM ~#hpn_۠Kw.akOuuup zNkejqP簩)^܄ ~ V`ڙv)J񹉺;]mrhL&S(F4mOdojɪU|w聮1R+U 2Djk+c Yo|ae23e`oyXZoenQΜ8\Ι3232U5^^^@W $]\\"}}G_xE-))).*wեAr KG+-)8C]u<[w{ jQ łtK]a \\O<>vjiSJKsxoM>aNNNAD{W"Plu>>swpդV[YY~jlBfFFF\AUeDSsǮ0hfwRiäc)*ѣGqU[Q#"Isr/Hb栁R)f̜>ýo~UoMqq uV`uNNlT*MNN؉R4Bl!aÆXn\Bicc#RMrssw8wL&2皵_8v8͚2o?k755YXoݺ@8:ݻc#ZAQ E(xzӦM07}vI;;L<`m訣l6ÏnG7ҳgyà(PQRHT&eӡGP?rԈE<\mgg缼<.g"##S:tْ3>}z]B;UU+PyHLL+vvvSNqp_WBl!+W7W$;E&^x_$<|gzZ+**7n$T'zy{$,\0n%%ώzERG źTۥ~ {{566FD-((ܺe.22@ ._^VqfUl6ݭBXhAIqM[T(\hÒQl2Def @T'&&5D/ @"8qJ6"I:QژVUєǰ G1Qr~~AhLݻF9p@hk@zZF/EQگ $r,K*_zQtظmccc91Ύgoos7$ICCCCCCo1tORңv -(]#dXXZXZZZ[[ݿfNs<;^߾}cŪZQo0mmmbcTM%}6xy{RGDCw999LלۮD5m``ˋi1OSS#r1uif6}:Sԑ3OsssPo=m*+W7Yreoߺu32bp8\ > fffFFF튁vEHA}1r]s9԰ۥ- 3SS52%/4?Bp b1Y,SS39Q /^jdf&N 6|Gv.А7nEnll  :b#[` v{c.s 3gpwwrֽ݆ML&^9!Ajjjd2Bl)Vlj-.P__VVV*Pui=sۥQmmXaXL&Rei'ŋàQ`k #z"Ϳ9B1&GzRk]"IW}];ߏDl6ۄ>~$=`6m_PhkcQ@*3KT*,]s72=-===@i\I:g޽'J= 0x40 && r <= 0x7E { inCSI = false } } else if r == '[' && prev == 0x1b { current.value = current.value[:utf8.RuneCountInString(current.value)-1] if current.value != "" { output = append(output, current) current = ansiSegment{} } inCSI = true current.style += "\x1b[" } else { current.value = current.value + string(r) } prev = r } if current.value != "" || current.style != "" { output = append(output, current) } return output } table-1.8.0/ansi_test.go000066400000000000000000000015161427645071700151470ustar00rootroot00000000000000package table import ( "testing" "github.com/stretchr/testify/assert" ) func Test_CSIStrip(t *testing.T) { tests := []struct { input string want string }{ { input: "hello world!", want: "hello world!", }, { input: "\x1b[37mhello \x1b[31mworld!", want: "hello world!", }, } for _, test := range tests { t.Run("", func(t *testing.T) { assert.Equal(t, test.want, newANSI(test.input).Strip()) }) } } func Test_ANSILen(t *testing.T) { tests := []struct { input string want int }{ { input: "hello world!", want: 12, }, { input: "\x1b[37mhello \x1b[31mworld!", want: 12, }, { input: "🔥 unicode 🔥 characters 🔥", want: 27, }, } for _, test := range tests { t.Run("", func(t *testing.T) { assert.Equal(t, test.want, newANSI(test.input).Len()) }) } } table-1.8.0/cmd/000077500000000000000000000000001427645071700133675ustar00rootroot00000000000000table-1.8.0/cmd/examples/000077500000000000000000000000001427645071700152055ustar00rootroot00000000000000table-1.8.0/cmd/examples/main.go000066400000000000000000000025331427645071700164630ustar00rootroot00000000000000package main import ( "bytes" "fmt" "io/ioutil" "os" "os/exec" "path/filepath" "strings" ) func main() { readme, err := ioutil.ReadFile("README.md") if err != nil { panic(err) } parts := strings.Split(string(readme), "") before := parts[0] parts = strings.Split(string(readme), "") after := parts[len(parts)-1] examples, err := os.ReadDir("_examples") if err != nil { panic(err) } var output string for _, example := range examples { if !example.IsDir() { continue } source, err := ioutil.ReadFile(filepath.Join("_examples", example.Name(), "main.go")) if err != nil { panic(err) } if strings.Contains(string(source), "noreadme") { continue } name := strings.Title(strings.ReplaceAll(example.Name(), "-", " ")[3:]) buffer := bytes.NewBuffer([]byte{}) cmd := exec.Command("go", "run", "."+string(filepath.Separator)+filepath.Join("_examples", example.Name())) cmd.Stdout = buffer cmd.Stderr = os.Stderr cwd, _ := os.Getwd() cmd.Dir = cwd if err := cmd.Run(); err != nil { panic(err) } output += fmt.Sprintf("\n### Example: %s\n```go\n%s\n```\n\n#### Output\n```\n%s\n```\n", name, string(source), buffer.String()) } output = before + "" + output + "" + after if err := ioutil.WriteFile("README.md", []byte(output), 0600); err != nil { panic(err) } } table-1.8.0/dividers.go000066400000000000000000000017511427645071700147700ustar00rootroot00000000000000package table type Dividers struct { ALL string NES string NSW string NEW string ESW string NE string NW string SW string ES string EW string NS string } var NoDividers = Dividers{} var UnicodeDividers = Dividers{ ALL: "┼", NES: "├", NSW: "┤", NEW: "┴", ESW: "┬", NE: "└", NW: "┘", SW: "┐", ES: "┌", EW: "─", NS: "│", } var UnicodeRoundedDividers = Dividers{ ALL: "┼", NES: "├", NSW: "┤", NEW: "┴", ESW: "┬", NE: "╰", NW: "╯", SW: "╮", ES: "╭", EW: "─", NS: "│", } var ASCIIDividers = Dividers{ ALL: "+", NES: "+", NSW: "+", NEW: "+", ESW: "+", NE: "+", NW: "+", SW: "+", ES: "+", EW: "-", NS: "|", } var StarDividers = Dividers{ ALL: "*", NES: "*", NSW: "*", NEW: "*", ESW: "*", NE: "*", NW: "*", SW: "*", ES: "*", EW: "*", NS: "*", } var MarkdownDividers = Dividers{ ALL: "|", NES: "|", NSW: "|", NE: "|", NW: "|", SW: "|", ES: "|", EW: "-", NS: "|", } table-1.8.0/go.mod000066400000000000000000000007271427645071700137400ustar00rootroot00000000000000module github.com/aquasecurity/table go 1.18 require ( github.com/mattn/go-runewidth v0.0.13 github.com/stretchr/testify v1.7.1 golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect ) table-1.8.0/go.sum000066400000000000000000000035171427645071700137650ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM= golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= table-1.8.0/style.go000066400000000000000000000011551427645071700143150ustar00rootroot00000000000000package table type Style int const ( StyleNormal Style = 0 StyleBold Style = 1 StyleDim Style = 2 StyleItalic Style = 3 StyleUnderline Style = 4 StyleBlack Style = 30 StyleRed Style = 31 StyleGreen Style = 32 StyleYellow Style = 33 StyleBlue Style = 34 StyleMagenta Style = 35 StyleCyan Style = 36 StyleWhite Style = 37 StyleBrightBlack Style = 90 StyleBrightRed Style = 91 StyleBrightGreen Style = 92 StyleBrightYellow Style = 93 StyleBrightBlue Style = 94 StyleBrightMagenta Style = 95 StyleBrightCyan Style = 96 StyleBrightWhite Style = 97 ) table-1.8.0/table.go000066400000000000000000000556571427645071700142640ustar00rootroot00000000000000package table import ( "encoding/csv" "fmt" "io" "os" "strings" runewidth "github.com/mattn/go-runewidth" "golang.org/x/term" ) // Table holds information required to render a table to the terminal type Table struct { w io.Writer data [][]string formatted []iRow headers [][]string footers [][]string alignments []Alignment headerAlignments []Alignment footerAlignments []Alignment borders Borders lineStyle Style dividers Dividers maxColumnWidth int padding int cursorStyle Style rowLines bool autoMerge bool autoMergeHeaders bool headerStyle Style headerColspans map[int][]int contentColspans map[int][]int footerColspans map[int][]int availableWidth int headerVerticalAlign Alignment fillWidth bool } type iRow struct { header bool footer bool cols []iCol first bool last bool height int } type iCol struct { original string span int lines []ansiBlob width int first bool last bool height int mergeAbove bool mergeBelow bool alignment Alignment } func (c iCol) MaxWidth() int { var maxWidth int for _, line := range c.lines { if line.Len() > maxWidth { maxWidth = line.Len() } } return maxWidth } // Borders dictates whether to draw lines at the extreme edges of the table type Borders struct { Left bool Top bool Right bool Bottom bool } // New creates a new Table func New(w io.Writer) *Table { availableWidth := 80 if w == os.Stdout { width, _, _ := term.GetSize(int(os.Stdout.Fd())) if width > 0 { availableWidth = width } } return &Table{ w: w, data: nil, headers: nil, footers: nil, alignments: nil, headerAlignments: nil, footerAlignments: nil, borders: Borders{ Left: true, Top: true, Right: true, Bottom: true, }, lineStyle: StyleNormal, dividers: UnicodeDividers, maxColumnWidth: 60, padding: 1, rowLines: true, autoMerge: false, headerColspans: make(map[int][]int), contentColspans: make(map[int][]int), footerColspans: make(map[int][]int), availableWidth: availableWidth, headerVerticalAlign: AlignTop, } } // SetBorders enables/disables the border around the table func (t *Table) SetBorders(enabled bool) { t.borders = Borders{ Left: enabled, Top: enabled, Right: enabled, Bottom: enabled, } } // SetBorderLeft enables/disables the border line on the left edge of the table func (t *Table) SetBorderLeft(enabled bool) { t.borders.Left = enabled } // SetBorderRight enables/disables the border line on the right edge of the table func (t *Table) SetBorderRight(enabled bool) { t.borders.Right = enabled } // SetBorderTop enables/disables the border line on the top edge of the table func (t *Table) SetBorderTop(enabled bool) { t.borders.Top = enabled } // SetBorderBottom enables/disables the border line on the bottom edge of the table func (t *Table) SetBorderBottom(enabled bool) { t.borders.Bottom = enabled } // SetLineStyle sets the ANSI style of the lines used when drawing the table. e.g. StyleYellow func (t *Table) SetLineStyle(s Style) { t.lineStyle = s } // SetRowLines sets whether to render horizontal lines between rows func (t *Table) SetRowLines(enabled bool) { t.rowLines = enabled } // SetAutoMerge sets whether to merge cells vertically if their content is the same and non-empty func (t *Table) SetAutoMerge(enabled bool) { t.autoMerge = enabled } // SetFillWidth sets whether to fill the entire available width func (t *Table) SetFillWidth(enabled bool) { t.fillWidth = enabled } // SetAutoMergeHeaders sets whether to merge header cells vertically if their content is the same and non-empty func (t *Table) SetAutoMergeHeaders(enabled bool) { t.autoMergeHeaders = enabled } // SetAlignment sets the alignment of each column. Should be specified for each column in the supplied data. // Default alignment for columns is AlignLeft func (t *Table) SetAlignment(columns ...Alignment) { t.alignments = columns } // SetHeaderAlignment sets the alignment of each header. Should be specified for each header in the supplied data. // Default alignment for headers is AlignCenter func (t *Table) SetHeaderAlignment(columns ...Alignment) { t.headerAlignments = columns } func (t *Table) SetHeaderVerticalAlignment(a Alignment) { t.headerVerticalAlign = a } // SetFooterAlignment sets the alignment of each footer. Should be specified for each footer in the supplied data. // Default alignment for footers is AlignCenter func (t *Table) SetFooterAlignment(columns ...Alignment) { t.footerAlignments = columns } // SetHeaders set the headers used for the table. func (t *Table) SetHeaders(headers ...string) { t.headers = [][]string{headers} } // AddHeaders adds a row of headers to the table. func (t *Table) AddHeaders(headers ...string) { t.headers = append(t.headers, headers) } // SetHeaderStyle set the style used for headers func (t *Table) SetHeaderStyle(s Style) { t.headerStyle = s } // SetFooters set the footers used for the table. func (t *Table) SetFooters(footers ...string) { t.footers = [][]string{footers} } // AddFooters adds a row of footers to the table. func (t *Table) AddFooters(footers ...string) { t.footers = append(t.footers, footers) } // SetPadding sets the minimum number of spaces which must surround each column value (horizontally). // For example, a padding of 3 would result in a column value such as " hello world " (3 spaces either side) func (t *Table) SetPadding(padding int) { t.padding = padding } // SetDividers allows customisation of the characters used to draw the table. // There are several built-in options, such as UnicodeRoundedDividers. // Specifying divider values containing more than 1 rune will result in undefined behaviour. func (t *Table) SetDividers(d Dividers) { t.dividers = d } // AddRow adds a row to the table. Each argument is a column value. func (t *Table) AddRow(cols ...string) { t.data = append(t.data, cols) } // SetAvailableWidth sets the available width for the table (defaults to terminal width when stdout is used) func (t *Table) SetAvailableWidth(w int) { t.availableWidth = w } // AddRows adds multiple rows to the table. Each argument is a row, i.e. a slice of column values. func (t *Table) AddRows(rows ...[]string) { t.data = append(t.data, rows...) } // SetColumnMaxWidth sets the max column width func (t *Table) SetColumnMaxWidth(maxColumnWidth int) { t.maxColumnWidth = maxColumnWidth } // LoadCSV loads CSV data from a reader and adds it to the table. Existing rows/headers/footers are retained. func (t *Table) LoadCSV(r io.Reader, hasHeaders bool) error { cr := csv.NewReader(r) if hasHeaders { headers, err := cr.Read() if err != nil { return err } t.SetHeaders(headers...) } for { data, err := cr.Read() if err != nil { if err == io.EOF { break } return err } t.AddRow(data...) } return nil } func (t *Table) getAlignment(colIndex int, header bool, footer bool) Alignment { switch { case header: if colIndex >= len(t.headerAlignments) { return AlignCenter } return t.headerAlignments[colIndex] case footer: if colIndex >= len(t.footerAlignments) { return AlignCenter } return t.footerAlignments[colIndex] default: if colIndex >= len(t.alignments) { return AlignLeft } return t.alignments[colIndex] } } // find the most columns we have in any given row, header, or footer func (t *Table) findMaxCols() int { var maxCols int for i, r := range t.headers { rowTotal := 0 for c := range r { rowTotal += t.getColspan(true, false, i, c) } if rowTotal > maxCols { maxCols = rowTotal } } for i, r := range t.data { rowTotal := 0 for c := range r { rowTotal += t.getColspan(false, false, i, c) } if rowTotal > maxCols { maxCols = rowTotal } } for i, r := range t.footers { rowTotal := 0 for c := range r { rowTotal += t.getColspan(false, true, i, c) } if rowTotal > maxCols { maxCols = rowTotal } } return maxCols } func (t *Table) formatData() { var formatted []iRow maxCols := t.findMaxCols() // add headers if len(t.headers) > 0 { for i, headerSet := range t.headers { headerRow := iRow{ header: true, footer: false, cols: nil, first: i == 0, last: i == len(t.headers)-1 && len(t.data)+len(t.footers) == 0, } for j, heading := range headerSet { headerRow.cols = append(headerRow.cols, iCol{ original: heading, width: runewidth.StringWidth(heading), first: j == 0, last: j == maxCols-1, alignment: t.getAlignment(j, true, false), span: t.getColspan(true, false, i, j), }) } formatted = append(formatted, headerRow) } } // add rows for rowIndex, cols := range t.data { fRow := iRow{ header: false, footer: false, cols: nil, first: rowIndex == 0 && len(formatted) == 0, last: rowIndex == len(t.data)-1 && len(t.footers) == 0, } for colIndex, data := range cols { fRow.cols = append(fRow.cols, iCol{ original: data, width: runewidth.StringWidth(data), first: colIndex == 0, last: colIndex == maxCols-1, alignment: t.getAlignment(colIndex, false, false), span: t.getColspan(false, false, rowIndex, colIndex), }) } formatted = append(formatted, fRow) } // add footers if len(t.footers) > 0 { for i, footerSet := range t.footers { footerRow := iRow{ header: false, footer: true, cols: nil, first: len(formatted) == 0, last: i == len(t.footers)-1, } for j, footing := range footerSet { footerRow.cols = append(footerRow.cols, iCol{ original: footing, width: runewidth.StringWidth(footing), first: j == 0, last: j == maxCols-1, alignment: t.getAlignment(j, false, true), span: t.getColspan(false, true, i, j), }) } formatted = append(formatted, footerRow) } } formatted = t.equaliseRows(formatted, maxCols) formatted = t.formatContent(formatted) t.formatted = t.mergeContent(formatted) } func (t *Table) calcColumnWidth(rowIndex int, row iRow) int { rowTotal := 0 for c := range row.cols { rowTotal += t.getColspan(row.header, row.footer, rowIndex, c) } return rowTotal } func (t *Table) equaliseRows(formatted []iRow, maxCols int) []iRow { // ensure all rows have the same number of columns (taking colspan into account) for i, row := range formatted { if len(row.cols) > 0 { row.cols[len(row.cols)-1].last = false } for t.calcColumnWidth(i, row) < maxCols { row.cols = append(row.cols, iCol{ first: len(row.cols) == 0, span: 1, }) } if len(row.cols) > 0 { row.cols[len(row.cols)-1].last = true } formatted[i] = row } return formatted } func (t *Table) formatContent(formatted []iRow) []iRow { var maxWidth int for _, row := range formatted { rowWidth := 1 for _, col := range row.cols { rowWidth += col.width + (t.padding * 2) + 1 } if rowWidth > maxWidth { maxWidth = rowWidth } } enableWrapping := t.availableWidth < maxWidth // wrap text for r, row := range formatted { maxLines := 0 for c, col := range row.cols { wrapLen := t.maxColumnWidth if !enableWrapping { wrapLen = runewidth.StringWidth(col.original) } wrapped := wrapText(col.original, wrapLen) formatted[r].cols[c].lines = wrapped if len(wrapped) > maxLines { maxLines = len(wrapped) } } // ensure all cols have the same number of lines for a given row for c, col := range row.cols { col.lines = t.alignVertically(col.lines, AlignTop, maxLines) col.height = len(col.lines) formatted[r].cols[c] = col } // set height for row formatted[r].height = maxLines } spares := make([]int, len(formatted)) for r, row := range formatted { spare := t.availableWidth - 1 for c, col := range row.cols { spare -= col.MaxWidth() + (t.getColspan(row.header, row.footer, r, c) * ((t.padding * 2) + 1)) } if spare < 0 { spare = 0 } spares[r] = spare } var extra int // set width of each col, and align text for c := 0; c < t.calcColumnWidth(0, formatted[0]); c++ { // for each col // find max width for column across all rows maxWidth := 0 for r, row := range formatted { if c >= len(row.cols) || row.cols[c].span > 1 { // ignore columns with a colspan > 1 for now, we'll apply those next continue } if t.fillWidth { extra = spares[r] / len(row.cols) } width := row.cols[c].MaxWidth() + extra if width > maxWidth { maxWidth = width } } // set uniform col width, and align all content for r, row := range formatted { if c >= len(row.cols) { continue } row.cols[c].width = maxWidth for l, line := range row.cols[c].lines { row.cols[c].lines[l] = align(line, maxWidth, row.cols[c].alignment) } formatted[r] = row } } return t.applyColSpans(formatted) } func (t *Table) alignVertically(lines []ansiBlob, alignment Alignment, maxLines int) []ansiBlob { switch alignment { case AlignBottom: for len(lines) < maxLines { lines = append([]ansiBlob{newANSI("")}, lines...) } default: for len(lines) < maxLines { lines = append(lines, newANSI("")) } } return lines } // e.g. 5 rows, 2nd with span 5, input 7 returns 3rd row func (t *Table) getRealIndex(row iRow, index int) int { var relative int for actual, col := range row.cols { if relative == index { return actual } relative += col.span } return len(row.cols) } func (t *Table) getRelativeIndex(row iRow, index int) int { var relative int for actual := 0; actual < len(row.cols) && actual < index; actual++ { relative += row.cols[actual].span } return relative } func (t *Table) applyColSpans(formatted []iRow) []iRow { type colSpanJob struct { row int relativeCol int span int } var jobs []colSpanJob for i, row := range formatted { for j, col := range row.cols { if col.span > 1 { jobs = append(jobs, colSpanJob{ row: i, relativeCol: t.getRelativeIndex(row, j), span: col.span, }) } } } for _, job := range jobs { realTargetColIndex := t.getRealIndex(formatted[job.row], job.relativeCol) // grab the cell that has a col span applied target := formatted[job.row].cols[realTargetColIndex] // calculate the width required for this cell targetWidth := target.MaxWidth() // calculate width required to render the cells beneath/above this cell - plus padding/dividers between them var childrenWidth int for i, row := range formatted { if i == job.row { // skip the row we're working on continue } var rowWidth int start := t.getRealIndex(row, job.relativeCol) stop := t.getRealIndex(row, job.relativeCol+job.span) for j := start; j < stop; j++ { rowWidth += row.cols[j].width } if rowWidth > childrenWidth { childrenWidth = rowWidth } } childrenWidth += (job.span - 1) * (1 + (2 * t.padding)) switch { case childrenWidth == targetWidth: // everything is somehow magically fine target.width = childrenWidth formatted[job.row].cols[realTargetColIndex] = target case childrenWidth > targetWidth: // we need to grow our target cell to make it aligned with the children for i := range target.lines { target.lines[i] = align(target.lines[i], childrenWidth, target.alignment) } target.width = childrenWidth formatted[job.row].cols[realTargetColIndex] = target default: // we need to extend the children to align with the wide cell // we can do this by sharing the extra space between them available := targetWidth - childrenWidth share := available / job.span remainder := available - (share * (job.span - 1)) // allocate each child some room for i, row := range formatted { if i == job.row { // skip the row we're working on continue } start := t.getRealIndex(row, job.relativeCol) stop := t.getRealIndex(row, job.relativeCol+job.span) for j := start; j < stop; j++ { amount := share if j == stop-1 { amount = remainder } row.cols[j].width += amount for k, line := range row.cols[j].lines { row.cols[j].lines[k] = align(line, row.cols[j].width, row.cols[j].alignment) } } formatted[i] = row } target.width = targetWidth formatted[job.row].cols[realTargetColIndex] = target } } var lastColMaxWidth int for r, row := range formatted { col := row.cols[len(row.cols)-1] if t.getColspan(row.header, row.footer, r, len(row.cols)-1) > 1 { continue } width := col.width if width > lastColMaxWidth { lastColMaxWidth = width } } for r, row := range formatted { c := len(row.cols) - 1 if t.getColspan(row.header, row.footer, r, c) > 1 { continue } width := row.cols[c].width if width < lastColMaxWidth { row.cols[c].width = lastColMaxWidth for k, line := range row.cols[c].lines { row.cols[c].lines[k] = align(line, lastColMaxWidth, row.cols[c].alignment) } formatted[r] = row } } return formatted } func (t *Table) mergeContent(formatted []iRow) []iRow { columnCount := t.calcColumnWidth(0, formatted[0]) lastValues := make([]string, columnCount) lastIndexes := make([]int, columnCount) // flag cols as mergeAbove where content matches and is non-empty for c := 0; c < columnCount; c++ { var prevHeader bool var allowed bool for r, row := range formatted { // don't merge columns with colspan > 1 if t.getColspan(row.header, row.footer, r, c) > 1 { continue } if c >= len(row.cols) { continue } relativeIndex := t.getRelativeIndex(row, c) allowed = (row.header && t.autoMergeHeaders) || (!row.header && !row.footer && !prevHeader && t.autoMerge) prevHeader = row.header current := row.cols[c].original merge := current == lastValues[relativeIndex] && strings.TrimSpace(current) != "" row.cols[c].mergeAbove = merge && allowed if merge && allowed { lastIndex := lastIndexes[relativeIndex] formatted[r-1].cols[lastIndex].mergeBelow = true if t.headerVerticalAlign == AlignBottom { buffer := row.cols[c].lines row.cols[c].lines = formatted[r-1].cols[lastIndex].lines formatted[r-1].cols[lastIndex].lines = buffer } } lastValues[relativeIndex] = current lastIndexes[relativeIndex] = c formatted[r] = row } } return formatted } func (t *Table) renderRows() { var lastRow iRow for _, row := range t.formatted { t.renderRow(row, lastRow) lastRow = row } } func (t *Table) renderRow(row iRow, prev iRow) { t.renderLineAbove(row, prev) for y := 0; y < row.height; y++ { if t.borders.Left { t.setStyle(t.lineStyle) t.print(t.dividers.NS) t.resetStyle() } for _, col := range row.cols { if t.padding > 0 { t.print(strings.Repeat(" ", t.padding)) } if col.mergeAbove && t.headerVerticalAlign == AlignTop { t.print(strings.Repeat(" ", col.width)) } else if col.mergeBelow && t.headerVerticalAlign == AlignBottom { t.print(strings.Repeat(" ", col.width)) } else { if row.header { t.setStyle(t.headerStyle) } t.print(col.lines[y].String()) if row.header { t.resetStyle() } } if t.padding > 0 { t.print(strings.Repeat(" ", t.padding)) } if t.borders.Right || !col.last { t.setStyle(t.lineStyle) t.print(t.dividers.NS) t.resetStyle() } } t.print("\n") } t.renderLineBelow(row) } // SetHeaderColSpans sets a column span for each column in the given header row. func (t *Table) SetHeaderColSpans(rowIndex int, colSpans ...int) { t.headerColspans[rowIndex] = colSpans } // SetColSpans sets a column span for each column in the given row. func (t *Table) SetColSpans(rowIndex int, colSpans ...int) { t.contentColspans[rowIndex] = colSpans } // SetFooterColSpans sets a column span for each column in the given footer row. func (t *Table) SetFooterColSpans(rowIndex int, colSpans ...int) { t.footerColspans[rowIndex] = colSpans } func (t *Table) getColspan(header bool, footer bool, row int, col int) int { var target map[int][]int switch { case header: target = t.headerColspans case footer: target = t.footerColspans default: target = t.contentColspans } r, ok := target[row] if !ok { return 1 } if col >= len(r) { return 1 } c := r[col] if c < 1 { return 1 } return c } // renders the line above a row func (t *Table) renderLineAbove(row iRow, prev iRow) { // don't draw top border if disabled if (row.first && !t.borders.Top) || (!prev.header && !t.rowLines && !row.first) { return } t.setStyle(t.lineStyle) for i, col := range row.cols { prevIsMerged := i > 0 && row.cols[i-1].mergeAbove relativeIndex := t.getRelativeIndex(row, i) var aboveIsSpanned bool for j, prevCol := range prev.cols { prevRel := t.getRelativeIndex(prev, j) if prevRel >= relativeIndex { break } if prevRel+prevCol.span > relativeIndex { aboveIsSpanned = true break } } switch { case col.first && !t.borders.Left: // hide border case row.first && col.first: t.print(t.dividers.ES) case row.first: t.print(t.dividers.ESW) case col.first && col.mergeAbove: t.print(t.dividers.NS) case col.first: t.print(t.dividers.NES) case col.mergeAbove && prevIsMerged: t.print(t.dividers.NS) case col.mergeAbove && !aboveIsSpanned: t.print(t.dividers.NSW) case col.mergeAbove: t.print(t.dividers.SW) case prevIsMerged && !aboveIsSpanned: t.print(t.dividers.NES) case prevIsMerged: t.print(t.dividers.ES) case aboveIsSpanned: t.print(t.dividers.ESW) default: t.print(t.dividers.ALL) } if col.mergeAbove { t.print(strings.Repeat(" ", col.width+(t.padding*2))) } else { t.print(strings.Repeat(t.dividers.EW, col.width+(t.padding*2))) } switch { case col.last && !t.borders.Right: // hide border case col.last && row.first: t.print(t.dividers.SW) case col.last && col.mergeAbove: t.print(t.dividers.NS) case col.last: t.print(t.dividers.NSW) } } t.resetStyle() t.print("\n") } // renders the line below a row, if required func (t *Table) renderLineBelow(row iRow) { // we only draw lines below the last row (if borders are on) if !row.last || !t.borders.Bottom { return } t.setStyle(t.lineStyle) for _, col := range row.cols { switch { case col.first && !t.borders.Left: // hide case col.first: t.print(t.dividers.NE) default: t.print(t.dividers.NEW) } t.print(strings.Repeat(t.dividers.EW, col.width+(t.padding*2))) if col.last && t.borders.Right { t.print(t.dividers.NW) } } t.resetStyle() t.print("\n") } func (t *Table) print(data string) { _, _ = fmt.Fprint(t.w, data) } func (t *Table) resetStyle() { t.setStyle(StyleNormal) } func (t *Table) setStyle(s Style) { if s != t.cursorStyle { _, _ = fmt.Fprintf(t.w, "\x1b[%dm", s) } t.cursorStyle = s } // Render writes the table to the provider io.Writer func (t *Table) Render() { if len(t.headers) == 0 && len(t.footers) == 0 && len(t.data) == 0 { return } t.formatData() t.renderRows() } table-1.8.0/table_test.go000066400000000000000000001146031427645071700153060ustar00rootroot00000000000000package table import ( "fmt" "strings" "testing" "github.com/stretchr/testify/assert" ) func assertMultilineEqual(t *testing.T, expected string, actual string) { if expected != actual { t.Errorf(`Error: Tables are not equal. Expected: %s Actual: %s `, expected, actual) } } func Test_BasicTable(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetHeaders("A", "B", "C") table.AddRow("1", "2", "3") table.AddRow("4", "5", "6") table.Render() assertMultilineEqual(t, ` ┌───┬───┬───┐ │ A │ B │ C │ ├───┼───┼───┤ │ 1 │ 2 │ 3 │ ├───┼───┼───┤ │ 4 │ 5 │ 6 │ └───┴───┴───┘ `, "\n"+builder.String()) } func Test_EmptyTable(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.Render() assertMultilineEqual(t, ``, builder.String()) } func Test_AddRows(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetHeaders("A", "B", "C") table.AddRows([]string{"1", "2", "3"}, []string{"4", "5", "6"}) table.Render() assertMultilineEqual(t, ` ┌───┬───┬───┐ │ A │ B │ C │ ├───┼───┼───┤ │ 1 │ 2 │ 3 │ ├───┼───┼───┤ │ 4 │ 5 │ 6 │ └───┴───┴───┘ `, "\n"+builder.String()) } func Test_NoHeaders(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.AddRow("1", "2", "3") table.AddRow("4", "5", "6") table.Render() assertMultilineEqual(t, ` ┌───┬───┬───┐ │ 1 │ 2 │ 3 │ ├───┼───┼───┤ │ 4 │ 5 │ 6 │ └───┴───┴───┘ `, "\n"+builder.String()) } func Test_Footers(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetFooters("A", "B", "C") table.AddRow("1", "2", "3") table.AddRow("4", "5", "6") table.Render() assertMultilineEqual(t, ` ┌───┬───┬───┐ │ 1 │ 2 │ 3 │ ├───┼───┼───┤ │ 4 │ 5 │ 6 │ ├───┼───┼───┤ │ A │ B │ C │ └───┴───┴───┘ `, "\n"+builder.String()) } func Test_VaryingWidths(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetHeaders("AAA", "BBBBB", "CCCCCCCCC") table.AddRow("111111", "2", "3") table.AddRow("4", "5555555555", "6") table.Render() assertMultilineEqual(t, ` ┌────────┬────────────┬───────────┐ │ AAA │ BBBBB │ CCCCCCCCC │ ├────────┼────────────┼───────────┤ │ 111111 │ 2 │ 3 │ ├────────┼────────────┼───────────┤ │ 4 │ 5555555555 │ 6 │ └────────┴────────────┴───────────┘ `, "\n"+builder.String()) } func Test_Wrapping(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetHeaders("ID", "Name", "Notes") table.AddRow("1", "Jim", "") table.AddRow("2", "Bob", "This is a very, very, very, very, very, long sentence which will surely wrap?") table.Render() assertMultilineEqual(t, ` ┌────┬──────┬─────────────────────────────────────────────────────────────┐ │ ID │ Name │ Notes │ ├────┼──────┼─────────────────────────────────────────────────────────────┤ │ 1 │ Jim │ │ ├────┼──────┼─────────────────────────────────────────────────────────────┤ │ 2 │ Bob │ This is a very, very, very, very, very, long sentence which │ │ │ │ will surely wrap? │ └────┴──────┴─────────────────────────────────────────────────────────────┘ `, "\n"+builder.String()) } func Test_MultipleLines(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetHeaders("ID", "Name", "Notes") table.AddRow("1", "Jim", "") table.AddRow("2", "Bob", "This is a sentence.\nThis is another sentence.\nAnd yet another one!") table.Render() assertMultilineEqual(t, ` ┌────┬──────┬───────────────────────────┐ │ ID │ Name │ Notes │ ├────┼──────┼───────────────────────────┤ │ 1 │ Jim │ │ ├────┼──────┼───────────────────────────┤ │ 2 │ Bob │ This is a sentence. │ │ │ │ This is another sentence. │ │ │ │ And yet another one! │ └────┴──────┴───────────────────────────┘ `, "\n"+builder.String()) } func Test_Padding_None(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetHeaders("ID", "Name", "Notes") table.AddRow("1", "Jim", "") table.AddRow("2", "Bob", "This is a sentence.\nThis is another sentence.\nAnd yet another one!") table.SetPadding(0) table.Render() assertMultilineEqual(t, ` ┌──┬────┬─────────────────────────┐ │ID│Name│ Notes │ ├──┼────┼─────────────────────────┤ │1 │Jim │ │ ├──┼────┼─────────────────────────┤ │2 │Bob │This is a sentence. │ │ │ │This is another sentence.│ │ │ │And yet another one! │ └──┴────┴─────────────────────────┘ `, "\n"+builder.String()) } func Test_Padding_Lots(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetHeaders("ID", "Name", "Notes") table.AddRow("1", "Jim", "Good") table.AddRow("2", "Bob", "Bad") table.SetPadding(10) table.Render() assertMultilineEqual(t, ` ┌──────────────────────┬────────────────────────┬─────────────────────────┐ │ ID │ Name │ Notes │ ├──────────────────────┼────────────────────────┼─────────────────────────┤ │ 1 │ Jim │ Good │ ├──────────────────────┼────────────────────────┼─────────────────────────┤ │ 2 │ Bob │ Bad │ └──────────────────────┴────────────────────────┴─────────────────────────┘ `, "\n"+builder.String()) } func Test_AlignmentDefaults(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetHeaders("ID", "Name", "Notes") table.AddRow("1", "Jim", "This person has a verrrrry short name.") table.AddRow("2", "Bob", "See above.") table.AddRow("99999", "John Verylongname", "Bad") table.Render() assertMultilineEqual(t, ` ┌───────┬───────────────────┬────────────────────────────────────────┐ │ ID │ Name │ Notes │ ├───────┼───────────────────┼────────────────────────────────────────┤ │ 1 │ Jim │ This person has a verrrrry short name. │ ├───────┼───────────────────┼────────────────────────────────────────┤ │ 2 │ Bob │ See above. │ ├───────┼───────────────────┼────────────────────────────────────────┤ │ 99999 │ John Verylongname │ Bad │ └───────┴───────────────────┴────────────────────────────────────────┘ `, "\n"+builder.String()) } func Test_AlignmentCustom(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetHeaders("ID", "Name", "Notes") table.SetFooters("ID", "Name", "Notes") table.SetHeaderAlignment(AlignLeft, AlignCenter, AlignRight) table.SetAlignment(AlignCenter, AlignRight, AlignLeft) table.SetFooterAlignment(AlignRight, AlignLeft, AlignCenter) table.AddRow("Please", "be", "aligned") table.Render() assertMultilineEqual(t, ` ┌────────┬──────┬─────────┐ │ ID │ Name │ Notes │ ├────────┼──────┼─────────┤ │ Please │ be │ aligned │ ├────────┼──────┼─────────┤ │ ID │ Name │ Notes │ └────────┴──────┴─────────┘ `, "\n"+builder.String()) } func Test_NonDefaultDividers(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetDividers(UnicodeRoundedDividers) table.SetHeaders("A", "B", "C") table.AddRow("1", "2", "3") table.AddRow("4", "5", "6") table.Render() assertMultilineEqual(t, ` ╭───┬───┬───╮ │ A │ B │ C │ ├───┼───┼───┤ │ 1 │ 2 │ 3 │ ├───┼───┼───┤ │ 4 │ 5 │ 6 │ ╰───┴───┴───╯ `, "\n"+builder.String()) } func Test_UnequalRows(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetHeaders("A", "B", "C") table.AddRow("1") table.AddRow() table.AddRow("", "2", "3") table.AddRow("4", "5", "6") table.AddRow("7", "8", "9", "10") table.Render() assertMultilineEqual(t, ` ┌───┬───┬───┬────┐ │ A │ B │ C │ │ ├───┼───┼───┼────┤ │ 1 │ │ │ │ ├───┼───┼───┼────┤ │ │ │ │ │ ├───┼───┼───┼────┤ │ │ 2 │ 3 │ │ ├───┼───┼───┼────┤ │ 4 │ 5 │ 6 │ │ ├───┼───┼───┼────┤ │ 7 │ 8 │ 9 │ 10 │ └───┴───┴───┴────┘ `, "\n"+builder.String()) } func Test_NoBorders(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetBorders(false) table.SetHeaders("A", "B", "C") table.AddRow("1", "2", "3") table.AddRow("4", "5", "6") table.Render() assertMultilineEqual(t, ` A │ B │ C ───┼───┼─── 1 │ 2 │ 3 ───┼───┼─── 4 │ 5 │ 6 `, "\n"+builder.String()) } func Test_NoLeftBorder(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetBorderLeft(false) table.SetHeaders("A", "B", "C") table.AddRow("1", "2", "3") table.AddRow("4", "5", "6") table.Render() assertMultilineEqual(t, ` ───┬───┬───┐ A │ B │ C │ ───┼───┼───┤ 1 │ 2 │ 3 │ ───┼───┼───┤ 4 │ 5 │ 6 │ ───┴───┴───┘ `, "\n"+builder.String()) } func Test_NoRightBorder(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetBorderRight(false) table.SetHeaders("A", "B", "C") table.AddRow("1", "2", "3") table.AddRow("4", "5", "6") table.Render() assertMultilineEqual(t, ` ┌───┬───┬─── │ A │ B │ C ├───┼───┼─── │ 1 │ 2 │ 3 ├───┼───┼─── │ 4 │ 5 │ 6 └───┴───┴─── `, "\n"+builder.String()) } func Test_NoTopBorder(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetBorderTop(false) table.SetHeaders("A", "B", "C") table.AddRow("1", "2", "3") table.AddRow("4", "5", "6") table.Render() assertMultilineEqual(t, ` │ A │ B │ C │ ├───┼───┼───┤ │ 1 │ 2 │ 3 │ ├───┼───┼───┤ │ 4 │ 5 │ 6 │ └───┴───┴───┘ `, "\n"+builder.String()) } func Test_NoBottomBorder(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetBorderBottom(false) table.SetHeaders("A", "B", "C") table.AddRow("1", "2", "3") table.AddRow("4", "5", "6") table.Render() assertMultilineEqual(t, ` ┌───┬───┬───┐ │ A │ B │ C │ ├───┼───┼───┤ │ 1 │ 2 │ 3 │ ├───┼───┼───┤ │ 4 │ 5 │ 6 │ `, "\n"+builder.String()) } func Test_LineStyle(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetLineStyle(StyleYellow) table.SetHeaders("A", "B", "C") table.AddRow("1", "2", "3") table.AddRow("4", "5", "6") table.Render() assert.Equal(t, "\n"+ "\x1b[33m┌───┬───┬───┐\x1b[0m\n"+ "\x1b[33m│\u001B[0m A \u001B[33m│\u001B[0m B \u001B[33m│\u001B[0m C \u001B[33m│\x1b[0m\n"+ "\x1b[33m├───┼───┼───┤\x1b[0m\n"+ "\x1b[33m│\u001B[0m 1 \u001B[33m│\u001B[0m 2 \u001B[33m│\u001B[0m 3 \u001B[33m│\x1b[0m\n"+ "\x1b[33m├───┼───┼───┤\x1b[0m\n"+ "\x1b[33m│\u001B[0m 4 \u001B[33m│\u001B[0m 5 \u001B[33m│\u001B[0m 6 \u001B[33m│\x1b[0m\n"+ "\x1b[33m└───┴───┴───┘\x1b[0m\n", "\n"+builder.String()) } func Test_PrerenderedANSI(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetHeaders("A", "B", "C") table.AddRow("1", "\x1b[37m2\x1b[0m", "3") table.AddRow("4", "5", "6") table.Render() assertMultilineEqual(t, ` ┌───┬───┬───┐ │ A │ B │ C │ ├───┼───┼───┤ │ 1 │ `+"\x1b[37m2\x1b[0m"+` │ 3 │ ├───┼───┼───┤ │ 4 │ 5 │ 6 │ └───┴───┴───┘ `, "\n"+builder.String()) } func Test_NoRowLines(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetRowLines(false) table.SetHeaders("A", "B", "C") table.AddRow("1", "2", "3") table.AddRow("4", "5", "6") table.AddRow("7", "8", "9") table.Render() assertMultilineEqual(t, ` ┌───┬───┬───┐ │ A │ B │ C │ ├───┼───┼───┤ │ 1 │ 2 │ 3 │ │ 4 │ 5 │ 6 │ │ 7 │ 8 │ 9 │ └───┴───┴───┘ `, "\n"+builder.String()) } func Test_AutoMerge(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetAutoMerge(true) table.SetHeaders("A", "B", "3") table.AddRow("", "2", "3") table.AddRow("", "2", "6") table.AddRow("1", "2", "6") table.Render() assertMultilineEqual(t, ` ┌───┬───┬───┐ │ A │ B │ 3 │ ├───┼───┼───┤ │ │ 2 │ 3 │ ├───┤ ├───┤ │ │ │ 6 │ ├───┤ │ │ │ 1 │ │ │ └───┴───┴───┘ `, "\n"+builder.String()) if t.Failed() { fmt.Println(builder.String()) } } func Test_Unicode(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetHeaders("A", "B", "C") table.AddRow("🔥 unicode 🔥 characters 🔥", "2", "3") table.AddRow("4", "5", "6") table.Render() /* The following may look wrong in your editor, but when double-width runes are rendered correctly, this is right. */ assertMultilineEqual(t, ` ┌─────────────────────────────┬───┬───┐ │ A │ B │ C │ ├─────────────────────────────┼───┼───┤ │ 🔥 unicode 🔥 characters 🔥 │ 2 │ 3 │ ├─────────────────────────────┼───┼───┤ │ 4 │ 5 │ 6 │ └─────────────────────────────┴───┴───┘ `, "\n"+builder.String()) } func TestCSV(t *testing.T) { input := strings.NewReader(`Id,Date,Message 1,2022-05-12,"Hello world!" 2,2022-05-12,"These messages are loaded from a CSV file." 3,2022-05-13,"Incredible!"`) builder := &strings.Builder{} table := New(builder) if err := table.LoadCSV(input, true); err != nil { panic(err) } table.Render() assertMultilineEqual(t, ` ┌────┬────────────┬────────────────────────────────────────────┐ │ Id │ Date │ Message │ ├────┼────────────┼────────────────────────────────────────────┤ │ 1 │ 2022-05-12 │ Hello world! │ ├────┼────────────┼────────────────────────────────────────────┤ │ 2 │ 2022-05-12 │ These messages are loaded from a CSV file. │ ├────┼────────────┼────────────────────────────────────────────┤ │ 3 │ 2022-05-13 │ Incredible! │ └────┴────────────┴────────────────────────────────────────────┘ `, "\n"+builder.String()) } func Test_MultipleHeaderRows(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetHeaders("A", "B", "C") table.AddHeaders("D", "E", "F") table.AddRow("1", "22", "333") table.AddRow("4444", "55555", "666666") table.Render() assertMultilineEqual(t, ` ┌──────┬───────┬────────┐ │ A │ B │ C │ ├──────┼───────┼────────┤ │ D │ E │ F │ ├──────┼───────┼────────┤ │ 1 │ 22 │ 333 │ ├──────┼───────┼────────┤ │ 4444 │ 55555 │ 666666 │ └──────┴───────┴────────┘ `, "\n"+builder.String()) } func Test_MultipleFooterRows(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetHeaders("A", "B", "C") table.AddHeaders("D", "E", "F") table.AddRow("1", "22", "333") table.AddRow("4444", "55555", "666666") table.SetFooters("G", "H", "I") table.AddFooters("J", "K", "L") table.Render() assertMultilineEqual(t, ` ┌──────┬───────┬────────┐ │ A │ B │ C │ ├──────┼───────┼────────┤ │ D │ E │ F │ ├──────┼───────┼────────┤ │ 1 │ 22 │ 333 │ ├──────┼───────┼────────┤ │ 4444 │ 55555 │ 666666 │ ├──────┼───────┼────────┤ │ G │ H │ I │ ├──────┼───────┼────────┤ │ J │ K │ L │ └──────┴───────┴────────┘ `, "\n"+builder.String()) } func Test_HeaderColSpan(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetHeaders("A", "B & C") table.SetHeaderColSpans(0, 1, 2) table.AddRow("1", "2", "3") table.AddRow("4", "5", "6") table.Render() assertMultilineEqual(t, ` ┌───┬───────┐ │ A │ B & C │ ├───┼───┬───┤ │ 1 │ 2 │ 3 │ ├───┼───┼───┤ │ 4 │ 5 │ 6 │ └───┴───┴───┘ `, "\n"+builder.String()) } func Test_HeaderColSpanLargerHeading(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetHeaders("A", "This is a long heading") table.SetHeaderColSpans(0, 1, 2) table.AddRow("1", "2", "3") table.AddRow("4", "5", "6") table.Render() assertMultilineEqual(t, ` ┌───┬────────────────────────┐ │ A │ This is a long heading │ ├───┼───────────┬────────────┤ │ 1 │ 2 │ 3 │ ├───┼───────────┼────────────┤ │ 4 │ 5 │ 6 │ └───┴───────────┴────────────┘ `, "\n"+builder.String()) } func Test_HeaderColSpanSmallerHeading(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetHeaders("A", "B") table.SetHeaderColSpans(0, 1, 2) table.AddRow("1", "2", "This is some long data") table.AddRow("4", "5", "6") table.Render() assertMultilineEqual(t, ` ┌───┬────────────────────────────┐ │ A │ B │ ├───┼───┬────────────────────────┤ │ 1 │ 2 │ This is some long data │ ├───┼───┼────────────────────────┤ │ 4 │ 5 │ 6 │ └───┴───┴────────────────────────┘ `, "\n"+builder.String()) } func Test_HeaderColSpanTrivyKubernetesStyle(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetHeaders("Namespace", "Resource", "Vulnerabilities", "Misconfigurations") table.AddHeaders("Namespace", "Resource", "Critical", "High", "Critical", "High") table.SetHeaderColSpans(0, 1, 1, 2, 2) table.SetAutoMergeHeaders(true) table.AddRow("default", "Deployment/app", "2", "5", "0", "3") table.AddRow("default", "Ingress/test", "-", "-", "1", "0") table.AddRow("default", "Service/test", "0", "0", "3", "0") table.Render() assertMultilineEqual(t, ` ┌───────────┬────────────────┬─────────────────┬───────────────────┐ │ Namespace │ Resource │ Vulnerabilities │ Misconfigurations │ │ │ ├──────────┬──────┼───────────┬───────┤ │ │ │ Critical │ High │ Critical │ High │ ├───────────┼────────────────┼──────────┼──────┼───────────┼───────┤ │ default │ Deployment/app │ 2 │ 5 │ 0 │ 3 │ ├───────────┼────────────────┼──────────┼──────┼───────────┼───────┤ │ default │ Ingress/test │ - │ - │ 1 │ 0 │ ├───────────┼────────────────┼──────────┼──────┼───────────┼───────┤ │ default │ Service/test │ 0 │ 0 │ 3 │ 0 │ └───────────┴────────────────┴──────────┴──────┴───────────┴───────┘ `, "\n"+builder.String()) } func Test_HeaderColSpanTrivyKubernetesStyleFull(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetHeaders("Namespace", "Resource", "Vulnerabilities", "Misconfigurations") table.AddHeaders("Namespace", "Resource", "Critical", "High", "Medium", "Low", "Unknown", "Critical", "High", "Medium", "Low", "Unknown") table.SetHeaderColSpans(0, 1, 1, 5, 5) table.SetAutoMergeHeaders(true) table.AddRow("default", "Deployment/app", "2", "5", "7", "8", "0", "0", "3", "5", "19", "0") table.AddRow("default", "Ingress/test", "-", "-", "-", "-", "-", "1", "0", "2", "17", "0") table.AddRow("default", "Service/test", "0", "0", "0", "1", "0", "3", "0", "4", "9", "0") table.Render() assertMultilineEqual(t, ` ┌───────────┬────────────────┬──────────────────────────────────────────┬──────────────────────────────────────────┐ │ Namespace │ Resource │ Vulnerabilities │ Misconfigurations │ │ │ ├──────────┬──────┬────────┬─────┬─────────┼──────────┬──────┬────────┬─────┬─────────┤ │ │ │ Critical │ High │ Medium │ Low │ Unknown │ Critical │ High │ Medium │ Low │ Unknown │ ├───────────┼────────────────┼──────────┼──────┼────────┼─────┼─────────┼──────────┼──────┼────────┼─────┼─────────┤ │ default │ Deployment/app │ 2 │ 5 │ 7 │ 8 │ 0 │ 0 │ 3 │ 5 │ 19 │ 0 │ ├───────────┼────────────────┼──────────┼──────┼────────┼─────┼─────────┼──────────┼──────┼────────┼─────┼─────────┤ │ default │ Ingress/test │ - │ - │ - │ - │ - │ 1 │ 0 │ 2 │ 17 │ 0 │ ├───────────┼────────────────┼──────────┼──────┼────────┼─────┼─────────┼──────────┼──────┼────────┼─────┼─────────┤ │ default │ Service/test │ 0 │ 0 │ 0 │ 1 │ 0 │ 3 │ 0 │ 4 │ 9 │ 0 │ └───────────┴────────────────┴──────────┴──────┴────────┴─────┴─────────┴──────────┴──────┴────────┴─────┴─────────┘ `, "\n"+builder.String()) } func Test_RelativeColIndexesSimple(t *testing.T) { row := iRow{ cols: []iCol{ { span: 1, }, { span: 1, }, { span: 1, }, }, } table := New(nil) assert.Equal(t, 0, table.getRealIndex(row, 0)) assert.Equal(t, 1, table.getRealIndex(row, 1)) assert.Equal(t, 2, table.getRealIndex(row, 2)) assert.Equal(t, 0, table.getRelativeIndex(row, 0)) assert.Equal(t, 1, table.getRelativeIndex(row, 1)) assert.Equal(t, 2, table.getRelativeIndex(row, 2)) } func Test_RelativeColIndexesWithSpans(t *testing.T) { row := iRow{ cols: []iCol{ { span: 2, }, { span: 3, }, { span: 1, }, }, } table := New(nil) assert.Equal(t, 1, table.getRealIndex(row, 2)) assert.Equal(t, 0, table.getRealIndex(row, 0)) assert.Equal(t, 2, table.getRealIndex(row, 5)) assert.Equal(t, 0, table.getRelativeIndex(row, 0)) assert.Equal(t, 2, table.getRelativeIndex(row, 1)) assert.Equal(t, 5, table.getRelativeIndex(row, 2)) } func Test_HeaderColSpanVariation(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetHeaders("Service", "Misconfigurations", "Last Scanned") table.AddHeaders("Service", "Critical", "High", "Medium", "Low", "Unknown", "Last Scanned") table.SetRowLines(false) table.SetHeaderAlignment(AlignLeft, AlignCenter, AlignCenter, AlignCenter, AlignCenter, AlignCenter, AlignLeft) table.SetAlignment(AlignLeft, AlignRight, AlignRight, AlignRight, AlignRight, AlignRight, AlignLeft) table.SetAutoMergeHeaders(true) table.SetHeaderColSpans(0, 1, 5, 1) table.AddRow("ec2", "1", "2", "5", "0", "3", "2 hours ago") table.AddRow("ecs", "0", "-", "-", "1", "0", "just now") table.AddRow("eks", "7", "0", "0", "3", "0", "127 hours ago") table.Render() assertMultilineEqual(t, ` ┌─────────┬──────────────────────────────────────────────────┬───────────────┐ │ Service │ Misconfigurations │ Last Scanned │ │ ├──────────┬──────────────┬────────┬─────┬─────────┤ │ │ │ Critical │ High │ Medium │ Low │ Unknown │ │ ├─────────┼──────────┼──────────────┼────────┼─────┼─────────┼───────────────┤ │ ec2 │ 1 │ 2 │ 5 │ 0 │ 3 │ 2 hours ago │ │ ecs │ 0 │ - │ - │ 1 │ 0 │ just now │ │ eks │ 7 │ 0 │ 0 │ 3 │ 0 │ 127 hours ago │ └─────────┴──────────┴──────────────┴────────┴─────┴─────────┴───────────────┘ `, "\n"+builder.String()) } func Test_HeaderVerticalAlignBottom(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetHeaders("Service", "Misconfigurations", "Last Scanned") table.AddHeaders("Service", "Critical", "High", "Medium", "Low", "Unknown", "Last Scanned") table.SetRowLines(false) table.SetHeaderAlignment(AlignLeft, AlignCenter, AlignCenter, AlignCenter, AlignCenter, AlignCenter, AlignLeft) table.SetHeaderVerticalAlignment(AlignBottom) table.SetAlignment(AlignLeft, AlignRight, AlignRight, AlignRight, AlignRight, AlignRight, AlignLeft) table.SetAutoMergeHeaders(true) table.SetHeaderColSpans(0, 1, 5, 1) table.AddRow("ec2", "1", "2", "5", "0", "3", "2 hours ago") table.AddRow("ecs", "0", "-", "-", "1", "0", "just now") table.AddRow("eks", "7", "0", "0", "3", "0", "127 hours ago") table.Render() assertMultilineEqual(t, ` ┌─────────┬──────────────────────────────────────────────────┬───────────────┐ │ │ Misconfigurations │ │ │ ├──────────┬──────────────┬────────┬─────┬─────────┤ │ │ Service │ Critical │ High │ Medium │ Low │ Unknown │ Last Scanned │ ├─────────┼──────────┼──────────────┼────────┼─────┼─────────┼───────────────┤ │ ec2 │ 1 │ 2 │ 5 │ 0 │ 3 │ 2 hours ago │ │ ecs │ 0 │ - │ - │ 1 │ 0 │ just now │ │ eks │ 7 │ 0 │ 0 │ 3 │ 0 │ 127 hours ago │ └─────────┴──────────┴──────────────┴────────┴─────┴─────────┴───────────────┘ `, "\n"+builder.String()) } func Test_FillWidth(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetHeaders("A", "B", "C") table.AddRow("1", "2", "3") table.AddRow("4", "5", "6") table.SetAvailableWidth(19) table.SetFillWidth(true) table.Render() assertMultilineEqual(t, ` ┌─────┬─────┬─────┐ │ A │ B │ C │ ├─────┼─────┼─────┤ │ 1 │ 2 │ 3 │ ├─────┼─────┼─────┤ │ 4 │ 5 │ 6 │ └─────┴─────┴─────┘ `, "\n"+builder.String()) } func Test_HeaderColSpanTrivyKubernetesStyleFullWithFillWidth(t *testing.T) { builder := &strings.Builder{} table := New(builder) table.SetHeaders("Namespace", "Resource", "Vulnerabilities", "Misconfigurations") table.AddHeaders("Namespace", "Resource", "Critical", "High", "Medium", "Low", "Unknown", "Critical", "High", "Medium", "Low", "Unknown") table.SetHeaderColSpans(0, 1, 1, 5, 5) table.SetAutoMergeHeaders(true) table.SetAvailableWidth(100) table.SetFillWidth(true) table.AddRow("default", "Deployment/app", "2", "5", "7", "8", "0", "0", "3", "5", "19", "0") table.AddRow("default", "Ingress/test", "-", "-", "-", "-", "-", "1", "0", "2", "17", "0") table.AddRow("default", "Service/test", "0", "0", "0", "1", "0", "3", "0", "4", "9", "0") table.Render() assertMultilineEqual(t, ` ┌──────────────┬──────────────────┬──────────────────────────────────────────┬───────────────────────────────────────────┐ │ Namespace │ Resource │ Vulnerabilities │ Misconfigurations │ │ │ ├──────────┬──────┬────────┬─────┬─────────┼──────────┬──────┬────────┬──────┬─────────┤ │ │ │ Critical │ High │ Medium │ Low │ Unknown │ Critical │ High │ Medium │ Low │ Unknown │ ├──────────────┼──────────────────┼──────────┼──────┼────────┼─────┼─────────┼──────────┼──────┼────────┼──────┼─────────┤ │ default │ Deployment/app │ 2 │ 5 │ 7 │ 8 │ 0 │ 0 │ 3 │ 5 │ 19 │ 0 │ ├──────────────┼──────────────────┼──────────┼──────┼────────┼─────┼─────────┼──────────┼──────┼────────┼──────┼─────────┤ │ default │ Ingress/test │ - │ - │ - │ - │ - │ 1 │ 0 │ 2 │ 17 │ 0 │ ├──────────────┼──────────────────┼──────────┼──────┼────────┼─────┼─────────┼──────────┼──────┼────────┼──────┼─────────┤ │ default │ Service/test │ 0 │ 0 │ 0 │ 1 │ 0 │ 3 │ 0 │ 4 │ 9 │ 0 │ └──────────────┴──────────────────┴──────────┴──────┴────────┴─────┴─────────┴──────────┴──────┴────────┴──────┴─────────┘ `, "\n"+builder.String()) } table-1.8.0/text.go000066400000000000000000000044571427645071700141510ustar00rootroot00000000000000package table import "strings" func align(input ansiBlob, width int, a Alignment) ansiBlob { padSize := width - input.Len() if padSize <= 0 { return input } switch a { case AlignRight: return newANSI(strings.Repeat(" ", padSize) + input.String()) case AlignCenter: leftPad := padSize / 2 rightPad := padSize - leftPad if leftPad > 0 { input = newANSI(strings.Repeat(" ", leftPad) + input.String()) } if rightPad > 0 { input = newANSI(input.String() + strings.Repeat(" ", rightPad)) } return input default: // left by default return newANSI(input.String() + strings.Repeat(" ", padSize)) } } func wrapText(input string, wrapSize int) []ansiBlob { var words []ansiBlob lines := strings.Split(input, "\n") for _, in := range lines { if len(words) > 0 { words = append(words, ansiBlob{{value: "\n"}}) } in = strings.TrimSpace(in) lineWords := newANSI(in).Words() for _, word := range lineWords { // word won't fit on a line by itself, so split it for word.Len() > wrapSize { before, after := word.Cut(wrapSize - 1) word = after words = append(words, newANSI(before.String()+"-")) } if word.Len() > 0 { words = append(words, word) } } } var output []ansiBlob var current ansiBlob for _, word := range words { available := wrapSize - current.Len() switch { case word.String() == "\n": output = append(output, current.TrimSpace()) current = newANSI("") case available == 0: // no room left on line, start a new one with the word added to it output = append(output, current.TrimSpace()) current = word if current.Len() < wrapSize { current = newANSI(current.String() + " ") } case current.Len()+word.Len() == wrapSize: // word fits on line exactly, add it current = newANSI(current.String() + word.String()) case current.Len()+word.Len() < wrapSize: // word fits on line, add it with a space afterwards current = newANSI(current.String() + word.String() + " ") default: // word won't fit so start a new line output = append(output, current.TrimSpace()) current = word if current.Len() < wrapSize { current = newANSI(current.String() + " ") } } } if current.Len() > 0 { output = append(output, current.TrimSpace()) } if len(output) == 0 { output = append(output, newANSI("")) } return output } table-1.8.0/text_test.go000066400000000000000000000034101427645071700151740ustar00rootroot00000000000000package table import ( "testing" "github.com/stretchr/testify/assert" ) func Test_TextWrapping(t *testing.T) { tests := []struct { name string input string wrap int want []string }{ { name: "no wrap required", input: "hello world!", wrap: 100, want: []string{"hello world!"}, }, { name: "basic wrap", input: "hello world!", wrap: 10, want: []string{"hello", "world!"}, }, { name: "exact length word", input: "hello incredible world!", wrap: 10, want: []string{"hello", "incredible", "world!"}, }, { name: "exact total fit", input: "it fit gud hooray", wrap: 10, want: []string{"it fit gud", "hooray"}, }, { name: "break word", input: "hello world, antidisestablishmentarianism!", wrap: 16, want: []string{"hello world,", "antidisestablis-", "hmentarianism!"}, }, { name: "new lines", input: "hello world\nthis is a\nlong sentence.", wrap: 10, want: []string{"hello", "world", "this is a", "long", "sentence."}, }, { name: "empty string", input: "", wrap: 10, want: []string{""}, }, { name: "multiple whitespace", input: "hello world!", wrap: 10, want: []string{"hello", "world!"}, }, { name: "ansi codes", input: "\x1b[37mhello this should be\x1b[38mover 4 lines!", wrap: 10, want: []string{"\x1b[37mhello this", "should", "be\x1b[38mover 4", "lines!"}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { output := wrapText(test.input, test.wrap) assert.Equal(t, test.want, blobsToStrings(output)) }) } } func blobsToStrings(blobs []ansiBlob) []string { var output []string for _, blob := range blobs { output = append(output, blob.String()) } return output }