pax_global_header00006660000000000000000000000064140725045420014515gustar00rootroot0000000000000052 comment=71ae72a78f03ea11c1df33a20dd20ac0d36c1aac go-pretty-6.2.4/000077500000000000000000000000001407250454200134605ustar00rootroot00000000000000go-pretty-6.2.4/.github/000077500000000000000000000000001407250454200150205ustar00rootroot00000000000000go-pretty-6.2.4/.github/ISSUE_TEMPLATE/000077500000000000000000000000001407250454200172035ustar00rootroot00000000000000go-pretty-6.2.4/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000010611407250454200216730ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior. Code samples if possible. **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Software (please complete the following information):** - OS: [e.g. OSX] - GoLang Version [e.g. 1.10] **Additional context** Add any other context about the problem here. go-pretty-6.2.4/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000010601407250454200227250ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. go-pretty-6.2.4/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000001151407250454200206160ustar00rootroot00000000000000## Proposed Changes - - - Fixes #. go-pretty-6.2.4/.github/workflows/000077500000000000000000000000001407250454200170555ustar00rootroot00000000000000go-pretty-6.2.4/.github/workflows/ci.yml000066400000000000000000000025351407250454200202000ustar00rootroot00000000000000name: CI on: # Pushes and pulls to all branches push: pull_request: # Run on the first day of every month schedule: - cron: "0 0 1 * *" # Allows you to run this workflow manually from the Actions tab workflow_dispatch: jobs: # Build and test everything build: runs-on: ubuntu-latest steps: # Checkout the code - name: Checkout Code uses: actions/checkout@v2 # Set up the GoLang enviroment - name: Set up Go uses: actions/setup-go@v2 with: go-version: 1.15 # Download all the tools used in the steps that follow - name: Set up Tools run: | go get -u github.com/fzipp/gocyclo/cmd/gocyclo go get -u github.com/mattn/goveralls go get -u golang.org/x/lint/golint # Run all the unit-tests - name: Test run: | make test # Run some tests to ensure no race conditions exist - name: Test for Race Conditions run: make test-race # Run the benchmarks to manually ensure no performance degradation - name: Benchmark run: make bench # Upload all the unit-test coverage reports to Coveralls - name: Upload Coverage Report env: COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: goveralls -service=github -coverprofile=.coverprofile go-pretty-6.2.4/.gitignore000066400000000000000000000000551407250454200154500ustar00rootroot00000000000000/.idea/ /demo* /profile/ .coverprofile *.swp go-pretty-6.2.4/CODE_OF_CONDUCT.md000066400000000000000000000062201407250454200162570ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at jedib0t@outlook.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ go-pretty-6.2.4/LICENSE000066400000000000000000000020501407250454200144620ustar00rootroot00000000000000MIT License Copyright (c) 2018 jedib0t 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. go-pretty-6.2.4/Makefile000066400000000000000000000011771407250454200151260ustar00rootroot00000000000000.PHONY: all profile test default: test all: test bench tools: go get github.com/fzipp/gocyclo go get golang.org/x/lint/golint bench: go test -bench=. -benchmem cyclo: gocyclo -over 13 ./*/*.go demo-list: go run cmd/demo-list/demo.go demo-progress: go run cmd/demo-progress/demo.go demo-table: go run cmd/demo-table/demo.go fmt: go fmt $(shell go list ./...) lint: golint -set_exit_status $(shell go list ./...) profile: sh profile.sh test: fmt lint vet cyclo go test -cover -coverprofile=.coverprofile $(shell go list ./...) test-race: go run -race ./cmd/demo-progress/demo.go vet: go vet $(shell go list ./...) go-pretty-6.2.4/README.md000066400000000000000000000122521407250454200147410ustar00rootroot00000000000000# go-pretty [![Go Reference](https://pkg.go.dev/badge/github.com/jedib0t/go-pretty/v6.svg)](https://pkg.go.dev/github.com/jedib0t/go-pretty/v6) [![Build Status](https://github.com/jedib0t/go-pretty/workflows/CI/badge.svg?branch=main)](https://github.com/jedib0t/go-pretty/actions?query=workflow%3ACI+event%3Apush+branch%3Amain) [![Coverage Status](https://coveralls.io/repos/github/jedib0t/go-pretty/badge.svg?branch=main)](https://coveralls.io/github/jedib0t/go-pretty?branch=main) [![Go Report Card](https://goreportcard.com/badge/github.com/jedib0t/go-pretty)](https://goreportcard.com/report/github.com/jedib0t/go-pretty) Utilities to prettify console output of tables, lists, progress-bars, text, etc. ## Table Pretty-print tables into ASCII/Unicode strings. ``` +-----+------------+-----------+--------+-----------------------------+ | # | FIRST NAME | LAST NAME | SALARY | | +-----+------------+-----------+--------+-----------------------------+ | 1 | Arya | Stark | 3000 | | | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | | 300 | Tyrion | Lannister | 5000 | | +-----+------------+-----------+--------+-----------------------------+ | | | TOTAL | 10000 | | +-----+------------+-----------+--------+-----------------------------+ ``` More details can be found here: [table/](table) ## List Pretty-print lists with multiple levels/indents into ASCII/Unicode strings. ``` ■ Game Of Thrones ■ Winter ■ Is ■ Coming ■ This ■ Is ■ Known ■ The Dark Tower ■ The Gunslinger ``` More details can be found here: [list/](list) # Progress Track the Progress of one or more Tasks (like downloading multiple files in parallel). Sample Progress Tracking: ``` Calculating Total # 1 ... done! [3.25K in 100ms] Calculating Total # 2 ... done! [6.50K in 100ms] Downloading File # 3 ... done! [9.75KB in 100ms] Transferring Amount # 4 ... done! [$26.00K in 200ms] Transferring Amount # 5 ... done! [£32.50K in 201ms] Downloading File # 6 ... done! [58.50KB in 300ms] Calculating Total # 7 ... done! [91.00K in 400ms] Transferring Amount # 8 ... 60.9% (●●●●●●●●●●●●●●◌◌◌◌◌◌◌◌◌) [$78.00K in 399.071ms] Downloading File # 9 ... 32.1% (●●●●●●●○◌◌◌◌◌◌◌◌◌◌◌◌◌◌◌) [58.50KB in 298.947ms] Transferring Amount # 10 ... 13.0% (●●○◌◌◌◌◌◌◌◌◌◌◌◌◌◌◌◌◌◌◌◌) [£32.50K in 198.84ms] ``` More details can be found here: [progress/](progress) ## Text Utility functions to manipulate text with or without ANSI escape sequences. Most of the functions available are used in one or more of the other packages here. - Align text horizontally or vertically - [text/align.go](text/align.go) and [text/valign.go](text/valign.go) - Colorize text - [text/color.go](text/color.go) - Cursor Movement - [text/cursor.go](text/cursor.go) - Format text (convert case) - [text/format.go](text/format.go) - String Manipulation (Pad, RepeatAndTrim, RuneCount, Trim, etc.) - [text/string.go](text/string.go) - Transform text (UnixTime to human-readable-time, pretty-JSON, etc.) - [text/transformer.go](text/transformer.go) - Wrap text - [text/wrap.go](text/wrap.go) The unit-tests for each of the above show how these can be used. There GoDoc should also have examples for all the available functions. ## Benchmarks Partial output of `make bench` on CI: ``` BenchmarkList_Render-2 372352 3179 ns/op 856 B/op 38 allocs/op BenchmarkProgress_Render-2 4 300318682 ns/op 3438 B/op 87 allocs/op BenchmarkTable_Render-2 27208 44154 ns/op 5616 B/op 179 allocs/op BenchmarkTable_RenderCSV-2 108732 11059 ns/op 2624 B/op 46 allocs/op BenchmarkTable_RenderHTML-2 88633 13425 ns/op 4080 B/op 45 allocs/op BenchmarkTable_RenderMarkdown-2 107420 10991 ns/op 2560 B/op 44 allocs/op ``` ## v6.0.0++ If you are using a version of this library older than `v6.0.0` and want to move to a newer version of this library, you'd have to modify the import paths from something like: ```golang "github.com/jedib0t/go-pretty/list" "github.com/jedib0t/go-pretty/progress" "github.com/jedib0t/go-pretty/table" "github.com/jedib0t/go-pretty/text" ``` to: ```golang "github.com/jedib0t/go-pretty/v6/list" "github.com/jedib0t/go-pretty/v6/progress" "github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/text" ``` I'd recommend you fire up your favorite IDE and do a mass search and replace for all occurrences of `jedib0t/go-pretty/` to `jedib0t/go-pretty/v6/`. If you are on a system with access to `find`, `grep`, `xargs` and `sed`, you could just run the following from within your code folder to do the same: ``` find . -type f -name "*.go" | grep -v vendor | xargs sed -i 's/jedib0t\/go-pretty\//jedib0t\/go-pretty\/v6\//'g ``` go-pretty-6.2.4/bench_test.go000066400000000000000000000047121407250454200161310ustar00rootroot00000000000000package gopretty import ( "io/ioutil" "testing" "time" "github.com/jedib0t/go-pretty/v6/list" "github.com/jedib0t/go-pretty/v6/progress" "github.com/jedib0t/go-pretty/v6/table" ) var ( listItem1 = "Game Of Thrones" listItems2 = []interface{}{"Winter", "Is", "Coming"} listItems3 = []interface{}{"This", "Is", "Known"} tableCaption = "table-caption" tableRowFooter = table.Row{"", "", "Total", 10000} tableRowHeader = table.Row{"#", "First Name", "Last Name", "Salary"} tableRows = []table.Row{ {1, "Arya", "Stark", 3000}, {20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"}, {300, "Tyrion", "Lannister", 5000}, } tracker1 = progress.Tracker{Message: "Calculating Total # 1", Total: 1000, Units: progress.UnitsDefault} tracker2 = progress.Tracker{Message: "Downloading File # 2", Total: 1000, Units: progress.UnitsBytes} tracker3 = progress.Tracker{Message: "Transferring Amount # 3", Total: 1000, Units: progress.UnitsCurrencyDollar} ) func BenchmarkList_Render(b *testing.B) { for i := 0; i < b.N; i++ { lw := list.NewWriter() lw.AppendItem(listItem1) lw.Indent() lw.AppendItems(listItems2) lw.Indent() lw.AppendItems(listItems3) lw.Render() } } func BenchmarkProgress_Render(b *testing.B) { trackSomething := func(pw progress.Writer, tracker *progress.Tracker) { tracker.Reset() pw.AppendTracker(tracker) time.Sleep(time.Millisecond * 100) tracker.Increment(tracker.Total / 2) time.Sleep(time.Millisecond * 100) tracker.Increment(tracker.Total / 2) } for i := 0; i < b.N; i++ { pw := progress.NewWriter() pw.SetAutoStop(true) pw.SetOutputWriter(ioutil.Discard) go trackSomething(pw, &tracker1) go trackSomething(pw, &tracker2) go trackSomething(pw, &tracker3) time.Sleep(time.Millisecond * 50) pw.Render() } } func generateBenchmarkTable() table.Writer { tw := table.NewWriter() tw.AppendHeader(tableRowHeader) tw.AppendRows(tableRows) tw.AppendFooter(tableRowFooter) tw.SetCaption(tableCaption) return tw } func BenchmarkTable_Render(b *testing.B) { for i := 0; i < b.N; i++ { generateBenchmarkTable().Render() } } func BenchmarkTable_RenderCSV(b *testing.B) { for i := 0; i < b.N; i++ { generateBenchmarkTable().RenderCSV() } } func BenchmarkTable_RenderHTML(b *testing.B) { for i := 0; i < b.N; i++ { generateBenchmarkTable().RenderHTML() } } func BenchmarkTable_RenderMarkdown(b *testing.B) { for i := 0; i < b.N; i++ { generateBenchmarkTable().RenderMarkdown() } } go-pretty-6.2.4/cmd/000077500000000000000000000000001407250454200142235ustar00rootroot00000000000000go-pretty-6.2.4/cmd/demo-list/000077500000000000000000000000001407250454200161205ustar00rootroot00000000000000go-pretty-6.2.4/cmd/demo-list/README.md000066400000000000000000000033451407250454200174040ustar00rootroot00000000000000Output of `go run cmd/demo-list/demo.go`: ``` A Simple List: -------------- * Game Of Thrones * The Dark Tower A Multi-level List: ------------------- * Game Of Thrones * Winter * Is * Coming * This * Is * Known * The Dark Tower * The Gunslinger A List using the Style 'StyleBulletCircle': ------------------------------------------- ● Game Of Thrones ● Winter ● Is ● Coming ● This ● Is ● Known ● The Dark Tower ● The Gunslinger A List using the Style 'StyleConnectedRounded': ----------------------------------------------- ╭─ Game Of Thrones │ ├─ Winter │ ├─ Is │ ╰─ Coming │ ├─ This │ ├─ Is │ ╰─ Known ╰─ The Dark Tower ╰─ The Gunslinger A List using the Style 'funkyStyle': ------------------------------------ t GAME OF THRONES |f WINTER |m IS |b COMING | f THIS | m IS | b KNOWN b THE DARK TOWER b THE GUNSLINGER A List in HTML format: ---------------------- [HTML] A List in Markdown format: -------------------------- [Markdown] * Game Of Thrones [Markdown] * Winter [Markdown] * Is [Markdown] * Coming [Markdown] * This [Markdown] * Is [Markdown] * Known [Markdown] * The Dark Tower [Markdown] * The Gunslinger ``` go-pretty-6.2.4/cmd/demo-list/demo.go000066400000000000000000000126451407250454200174030ustar00rootroot00000000000000package main import ( "fmt" "strings" "github.com/jedib0t/go-pretty/v6/list" "github.com/jedib0t/go-pretty/v6/text" ) func demoPrint(title string, content string, prefix string) { fmt.Printf("%s:\n", title) fmt.Println(strings.Repeat("-", len(title)+1)) for _, line := range strings.Split(content, "\n") { fmt.Printf("%s%s\n", prefix, line) } fmt.Println() } func main() { //========================================================================== // Initialization //========================================================================== l := list.NewWriter() // you can also instantiate the object directly lTemp := list.List{} lTemp.Render() // just to avoid the compile error of not using the object //========================================================================== //========================================================================== // A List needs Items. //========================================================================== l.AppendItem("Game Of Thrones") l.AppendItem("The Dark Tower") demoPrint("A Simple List", l.Render(), "") //A Simple List: //-------------- //* Game Of Thrones //* The Dark Tower l.Reset() //========================================================================== //========================================================================== // I wanna Level Down! //========================================================================== l.AppendItem("Game Of Thrones") l.Indent() l.AppendItems([]interface{}{"Winter", "Is", "Coming"}) l.Indent() l.AppendItems([]interface{}{"This", "Is", "Known"}) l.UnIndent() l.UnIndent() l.AppendItem("The Dark Tower") l.Indent() l.AppendItem("The Gunslinger") demoPrint("A Multi-level List", l.Render(), "") //A Multi-level List: //------------------- //* Game Of Thrones // * Winter // * Is // * Coming // * This // * Is // * Known //* The Dark Tower // * The Gunslinger //========================================================================== //========================================================================== // I am Fancy! //========================================================================== l.SetStyle(list.StyleBulletCircle) demoPrint("A List using the Style 'StyleBulletCircle'", l.Render(), "") //A List using the Style 'StyleBulletCircle': //------------------------------------------- //● Game Of Thrones // ● Winter // ● Is // ● Coming // ● This // ● Is // ● Known //● The Dark Tower // ● The Gunslinger l.SetStyle(list.StyleConnectedRounded) demoPrint("A List using the Style 'StyleConnectedRounded'", l.Render(), "") //A List using the Style 'StyleConnectedRounded': //----------------------------------------------- //╭─ Game Of Thrones //├─┬─ Winter //│ ├─ Is //│ ├─ Coming //│ ╰─┬─ This //│ ├─ Is //│ ╰─ Known //├─ The Dark Tower //╰─── The Gunslinger //========================================================================== //========================================================================== // I want my own Style! //========================================================================== funkyStyle := list.Style{ CharItemSingle: "s", CharItemTop: "t", CharItemFirst: "f", CharItemMiddle: "m", CharItemVertical: "|", CharItemBottom: "b", CharNewline: "\n", Format: text.FormatUpper, LinePrefix: "", Name: "styleTest", } l.SetStyle(funkyStyle) demoPrint("A List using the Style 'funkyStyle'", l.Render(), "") //A List using the Style 'funkyStyle': //------------------------------------ //t GAME OF THRONES //|f WINTER //|m IS //|b COMING //| f THIS //| m IS //| b KNOWN //b THE DARK TOWER // b THE GUNSLINGER //========================================================================== //========================================================================== // I want to use it in a HTML file! //========================================================================== demoPrint("A List in HTML format", l.RenderHTML(), "[HTML] ") //A List in HTML format: //---------------------- //[HTML] //========================================================================== //========================================================================== // Can I get the list in Markdown format? //========================================================================== demoPrint("A List in Markdown format", l.RenderMarkdown(), "[Markdown] ") fmt.Println() //A List in Markdown format: //-------------------------- //[Markdown] * Game Of Thrones //[Markdown] * Winter //[Markdown] * Is //[Markdown] * Coming //[Markdown] * This //[Markdown] * Is //[Markdown] * Known //[Markdown] * The Dark Tower //[Markdown] * The Gunslinger //========================================================================== } go-pretty-6.2.4/cmd/demo-progress/000077500000000000000000000000001407250454200170115ustar00rootroot00000000000000go-pretty-6.2.4/cmd/demo-progress/README.md000066400000000000000000000017071407250454200202750ustar00rootroot00000000000000Output of `go run cmd/demo-list/demo.go`: ``` Tracking Progress of 13 trackers ... Calculating Total # 1 ... done! [250 in 101ms] Calculating Total # 2 ... done! [2.00K in 101ms] Downloading File # 3 ... done! [6.75KB in 101ms] Transferring Amount # 4 ... done! [$16.00K in 200ms] Transferring Amount # 5 ... done! [£31.25K in 201ms] Downloading File # 6 ... done! [54.00KB in 300ms] Calculating Total # 7 ... done! [85.75K in 400ms] Transferring Amount # 8 ... done! [$128.00K in 500ms] Downloading File # 9 ... done! [182.25KB in 700ms] Transferring Amount # 10 ... done! [£250.00K in 801ms] Calculating Total # 11 ... done! [332.75K in 1s] Transferring Amount # 12 ... done! [$432.00K in 1.2s] Calculating Total # 13 ... done! [549.25K in 1.301s] All done! ``` Real-time playback of the demo @ asciinema.org: [![asciicast](https://asciinema.org/a/KcPw8aoBSsYCBOj60wluhu5z3.png)](https://asciinema.org/a/KcPw8aoBSsYCBOj60wluhu5z3) go-pretty-6.2.4/cmd/demo-progress/demo.go000066400000000000000000000073471407250454200202770ustar00rootroot00000000000000package main import ( "flag" "fmt" "math/rand" "time" "github.com/jedib0t/go-pretty/v6/progress" "github.com/jedib0t/go-pretty/v6/text" ) var ( autoStop = flag.Bool("auto-stop", false, "Auto-stop rendering?") randomFail = flag.Bool("rnd-fail", false, "Enable random failures") numTrackers = flag.Int("num-trackers", 13, "Number of Trackers") messageColors = []text.Color{ text.FgRed, text.FgGreen, text.FgYellow, text.FgBlue, text.FgMagenta, text.FgCyan, text.FgWhite, } ) func trackSomething(pw progress.Writer, idx int64, updateMessage bool) { total := idx * idx * idx * 250 incrementPerCycle := idx * int64(*numTrackers) * 250 var units *progress.Units switch { case idx%5 == 0: units = &progress.UnitsCurrencyPound case idx%4 == 0: units = &progress.UnitsCurrencyDollar case idx%3 == 0: units = &progress.UnitsBytes default: units = &progress.UnitsDefault } var message string switch units { case &progress.UnitsBytes: message = fmt.Sprintf("Downloading File #%3d", idx) case &progress.UnitsCurrencyDollar, &progress.UnitsCurrencyEuro, &progress.UnitsCurrencyPound: message = fmt.Sprintf("Transferring Amount #%3d", idx) default: message = fmt.Sprintf("Calculating Total #%3d", idx) } tracker := progress.Tracker{Message: message, Total: total, Units: *units} if idx == int64(*numTrackers) { tracker.Total = 0 } pw.AppendTracker(&tracker) ticker := time.Tick(time.Millisecond * 500) updateTicker := time.Tick(time.Millisecond * 250) for !tracker.IsDone() { select { case <-ticker: tracker.Increment(incrementPerCycle) if idx == int64(*numTrackers) && tracker.Value() >= total { tracker.MarkAsDone() } else if *randomFail && rand.Float64() < 0.1 { tracker.MarkAsErrored() } case <-updateTicker: if updateMessage { rndIdx := rand.Intn(len(messageColors)) if rndIdx == len(messageColors) { rndIdx-- } tracker.UpdateMessage(messageColors[rndIdx].Sprint(message)) } } } } func main() { flag.Parse() fmt.Printf("Tracking Progress of %d trackers ...\n\n", *numTrackers) // instantiate a Progress Writer and set up the options pw := progress.NewWriter() pw.SetAutoStop(*autoStop) pw.SetTrackerLength(25) pw.ShowETA(true) pw.ShowOverallTracker(true) pw.ShowTime(true) pw.ShowTracker(true) pw.ShowValue(true) pw.SetMessageWidth(24) pw.SetNumTrackersExpected(*numTrackers) pw.SetSortBy(progress.SortByPercentDsc) pw.SetStyle(progress.StyleDefault) pw.SetTrackerPosition(progress.PositionRight) pw.SetUpdateFrequency(time.Millisecond * 100) pw.Style().Colors = progress.StyleColorsExample pw.Style().Options.PercentFormat = "%4.1f%%" // call Render() in async mode; yes we don't have any trackers at the moment go pw.Render() // add a bunch of trackers with random parameters to demo most of the // features available; do this in async too like a client might do (for ex. // when downloading a bunch of files in parallel) for idx := int64(1); idx <= int64(*numTrackers); idx++ { go trackSomething(pw, idx, idx == int64(*numTrackers)) // in auto-stop mode, the Render logic terminates the moment it detects // zero active trackers; but in a manual-stop mode, it keeps waiting and // is a good chance to demo trackers being added dynamically while other // trackers are active or done if !*autoStop { time.Sleep(time.Millisecond * 100) } } // wait for one or more trackers to become active (just blind-wait for a // second) and then keep watching until Rendering is in progress time.Sleep(time.Second) for pw.IsRenderInProgress() { // for manual-stop mode, stop when there are no more active trackers if !*autoStop && pw.LengthActive() == 0 { pw.Stop() } time.Sleep(time.Millisecond * 100) } fmt.Println("\nAll done!") } go-pretty-6.2.4/cmd/demo-table/000077500000000000000000000000001407250454200162345ustar00rootroot00000000000000go-pretty-6.2.4/cmd/demo-table/README.md000066400000000000000000000573771407250454200175360ustar00rootroot00000000000000Output of `go run cmd/demo-table/demo.go`: ``` +-----+--------+-----------+------+-----------------------------+ | 1 | Arya | Stark | 3000 | | | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | | 300 | Tyrion | Lannister | 5000 | | +-----+--------+-----------+------+-----------------------------+ Simple Table with 3 Rows. +---+-----+--------+-----------+------+-----------------------------+ | | A | B | C | D | E | +---+-----+--------+-----------+------+-----------------------------+ | 1 | 1 | Arya | Stark | 3000 | | | 2 | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | | 3 | 300 | Tyrion | Lannister | 5000 | | +---+-----+--------+-----------+------+-----------------------------+ Table with Auto-Indexing. +---+-----+------------+-----------+--------+-----------------------------+ | | # | FIRST NAME | LAST NAME | SALARY | | +---+-----+------------+-----------+--------+-----------------------------+ | 1 | 1 | Arya | Stark | 3000 | | | 2 | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | | 3 | 300 | Tyrion | Lannister | 5000 | | +---+-----+------------+-----------+--------+-----------------------------+ Table with Auto-Indexing (columns-only). +-----+------------+-----------+--------+-----------------------------+ | # | FIRST NAME | LAST NAME | SALARY | | +-----+------------+-----------+--------+-----------------------------+ | 1 | Arya | Stark | 3000 | | | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | | 300 | Tyrion | Lannister | 5000 | | +-----+------------+-----------+--------+-----------------------------+ Table with 3 Rows & and a Header. +-----+------------+-----------+--------+-----------------------------+ | # | FIRST NAME | LAST NAME | SALARY | | +-----+------------+-----------+--------+-----------------------------+ | 1 | Arya | Stark | 3000 | | | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | | 300 | Tyrion | Lannister | 5000 | | +-----+------------+-----------+--------+-----------------------------+ | | | TOTAL | 10000 | | +-----+------------+-----------+--------+-----------------------------+ Table with 3 Rows, a Header & a Footer. +-----+------------+-----------+--------+-----------------------------+ | # | FIRST NAME | LAST NAME | SALARY | | +-----+------------+-----------+--------+-----------------------------+ | 1 | Arya | Stark | 3000 | | | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | | 300 | Tyrion | Lannister | 5000 | | | 4 | Faceless | Man | 0 | Needs a name. | +-----+------------+-----------+--------+-----------------------------+ | | | TOTAL | 10000 | | +-----+------------+-----------+--------+-----------------------------+ Table with Custom Alignment for 2 columns. +-----+------------+-----------+--------+-----------------------------+ | # | FIRST NAME | LAST NAME | SALARY | | +-----+------------+-----------+--------+-----------------------------+ | 1 | Arya | Stark | 3000 | | | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | | 300 | Tyrion | Lannister | 5000 | | | 4 | Faceless | Man | 0 | Needs a name. | | 13 | Winter | Valar | 0 | You | | | Is | Morghulis | | know | | | Coming | | | nothing, | | | | | | Jon | | | | | | Snow! | +-----+------------+-----------+--------+-----------------------------+ | | | TOTAL | 10000 | | +-----+------------+-----------+--------+-----------------------------+ Table with a Multi-line Row. +-----+------------+-----------+--------+-----------------------------+ | # | FIRST NAME | LAST NAME | SALARY | | +-----+------------+-----------+--------+-----------------------------+ | 1 | Arya | Stark | 3000 | | | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | | 300 | Tyrion | Lannister | 5000 | | | 4 | Faceless | Man | 0 | Needs a name. | | 13 | | | | You | | | Winter | | | know | | | Is | | 0 | nothing, | | | Coming | Valar | | Jon | | | | Morghulis | | Snow! | +-----+------------+-----------+--------+-----------------------------+ | | | TOTAL | 10000 | | +-----+------------+-----------+--------+-----------------------------+ Table with a Multi-line Row with VAlign. +-----+------------+-----------+--------+-----------------------------+ | # | FIRST NAME | LAST NAME | SALARY | | +-----+------------+-----------+--------+-----------------------------+ | 1 | Arya | Stark | 3000 | | | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | | 300 | Tyrion | Lannister | 5000 | | | 4 | Faceless | Man | 0 | Needs a name. | | 13 | | | | You | | | Winter | | | know | | | Is | | 0 | nothing, | | | Coming | Valar | | Jon | | | | Morghulis | | Snow! | +-----+------------+-----------+--------+-----------------------------+ | | | TOTAL | 10000 | | +-----+------------+-----------+--------+-----------------------------+ Table with a Multi-line Row with VAlign and changed Align. +-----+------------+-----------+--------+-----------------------------+ | # | FIRST NAME | LAST NAME | SALARY | | +-----+------------+-----------+--------+-----------------------------+ | 1 | Arya | Stark | 3000 | | | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | +-----+------------+-----------+--------+-----------------------------+ | 300 | Tyrion | Lannister | 5000 | | +-----+------------+-----------+--------+-----------------------------+ | | | TOTAL | 10000 | | +-----+------------+-----------+--------+-----------------------------+ Simple Table with 3 Rows and a Separator in-between. +-----+------------+-----------+--------+-----------------------------+ | # | FIRST NAME | LAST NAME | SALARY | | +-----+------------+-----------+--------+-----------------------------+ | 1 | Arya | Stark | 3000 | | | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | | 300 | Tyrion | Lannister | 5000 | | +-----+------------+-----------+--------+-----------------------------+ | | | TOTAL | 10000 | | +-----+------------+-----------+--------+-----------------------------+ Starting afresh with a Simple Table again. +-----+------------+-----------+--------+-----------------------------+ | # | FIRST NAME | LAST NAME | SALARY | | +-----+------------+-----------+--------+-----------------------------+ | 1 | Arya | Stark | 3000 | | +-----+------------+-----------+--------+-----------------------------+ | | | TOTAL | 10000 | | +-----+------------+-----------+--------+-----------------------------+ ... page break ... +-----+------------+-----------+--------+-----------------------------+ | # | FIRST NAME | LAST NAME | SALARY | | +-----+------------+-----------+--------+-----------------------------+ | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | +-----+------------+-----------+--------+-----------------------------+ | | | TOTAL | 10000 | | +-----+------------+-----------+--------+-----------------------------+ ... page break ... +-----+------------+-----------+--------+-----------------------------+ | # | FIRST NAME | LAST NAME | SALARY | | +-----+------------+-----------+--------+-----------------------------+ | 300 | Tyrion | Lannister | 5000 | | +-----+------------+-----------+--------+-----------------------------+ | | | TOTAL | 10000 | | +-----+------------+-----------+--------+-----------------------------+ Table with a PageSize of 1. +-----+------------+-----------+--------+------- ~ | # | FIRST NAME | LAST NAME | SALARY | ~ +-----+------------+-----------+--------+------- ~ | 1 | Arya | Stark | 3000 | ~ | 20 | Jon | Snow | 2000 | You kn ~ | 300 | Tyrion | Lannister | 5000 | ~ +-----+------------+-----------+--------+------- ~ | | | TOTAL | 10000 | ~ +-----+------------+-----------+--------+------- ~ Table with an Allowed Row Length of 50. ╔═════╦════════════╦═══════════╦════════╦═══════ ≈ ║ # ║ FIRST NAME ║ LAST NAME ║ SALARY ║ ≈ ╠═════╬════════════╬═══════════╬════════╬═══════ ≈ ║ 1 ║ Arya ║ Stark ║ 3000 ║ ≈ ║ 20 ║ Jon ║ Snow ║ 2000 ║ You kn ≈ ║ 300 ║ Tyrion ║ Lannister ║ 5000 ║ ≈ ╠═════╬════════════╬═══════════╬════════╬═══════ ≈ ║ ║ ║ TOTAL ║ 10000 ║ ≈ ╚═════╩════════════╩═══════════╩════════╩═══════ ≈ Table with an Allowed Row Length of 50 in 'StyleDouble'. ╭─────┬────────┬───────────┬────────┬────────────╮ │ # │ FIRST │ LAST NAME │ SALARY │ │ │ │ NAME │ │ │ │ ├─────┼────────┼───────────┼────────┼────────────┤ │ 1 │ Arya │ Stark │ 3000 │ │ │ 20 │ Jon │ Snow │ 2000 │ You know n │ │ │ │ │ │ othing, Jo │ │ │ │ │ │ n Snow! │ │ 300 │ Tyrion │ Lannister │ 5000 │ │ ├─────┼────────┼───────────┼────────┼────────────┤ │ │ │ TOTAL │ 10000 │ │ ╰─────┴────────┴───────────┴────────┴────────────╯ Table on a diet. ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐ │ # │ FIRST NAME │ LAST NAME │ SALARY │ │ ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ │ 1 │ Arya │ Stark │ 3000 │ │ │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ │ 300 │ Tyrion │ Lannister │ 5000 │ │ ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ │ │ │ TOTAL │ 10000 │ │ └─────┴────────────┴───────────┴────────┴─────────────────────────────┘ Table using the style 'StyleLight'. ╔═════╦════════════╦═══════════╦════════╦═════════════════════════════╗ ║ # ║ FIRST NAME ║ LAST NAME ║ SALARY ║ ║ ╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣ ║ 1 ║ Arya ║ Stark ║ 3000 ║ ║ ║ 20 ║ Jon ║ Snow ║ 2000 ║ You know nothing, Jon Snow! ║ ║ 300 ║ Tyrion ║ Lannister ║ 5000 ║ ║ ╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣ ║ ║ ║ TOTAL ║ 10000 ║ ║ ╚═════╩════════════╩═══════════╩════════╩═════════════════════════════╝ Table using the style 'StyleDouble'. (-----^------------^-----------^--------^-----------------------------) [< #>||||< >] {-----+------------+-----------+--------+-----------------------------} [< 1>|||< 3000>|< >] [< 20>|||< 2000>|] [<300>|||< 5000>|< >] {-----+------------+-----------+--------+-----------------------------} [< >|< >||< 10000>|< >] \-----v------------v-----------v--------v-----------------------------/ Table using the style 'funkyStyle'. ┏━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ # ┃ FIRST NAME ┃ LAST NAME ┃ SALARY ┃ ┃ ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ ┃ 1 ┃ Arya ┃ Stark ┃ 3000 ┃ ┃ ┃ 20 ┃ Jon ┃ Snow ┃ 2000 ┃ You know nothing, Jon Snow! ┃ ┃ 300 ┃ Tyrion ┃ Lannister ┃ 5000 ┃ ┃ ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ ┃ ┃ ┃ TOTAL ┃ 10000 ┃ ┃ ┗━━━━━┻━━━━━━━━━━━━┻━━━━━━━━━━━┻━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Table with Colors. "┏━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓" "┃\x1b[47;30m # \x1b[0m┃\x1b[47;30m FIRST NAME \x1b[0m┃\x1b[47;30m LAST NAME \x1b[0m┃\x1b[47;30m SALARY \x1b[0m┃\x1b[47;30m \x1b[0m┃" "┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫" "┃\x1b[33m 1 \x1b[0m┃\x1b[91m Arya \x1b[0m┃\x1b[91m Stark \x1b[0m┃\x1b[32m 3000 \x1b[0m┃\x1b[36m \x1b[0m┃" "┃\x1b[33m 20 \x1b[0m┃\x1b[91m Jon \x1b[0m┃\x1b[91m Snow \x1b[0m┃\x1b[32m 2000 \x1b[0m┃\x1b[36m You know nothing, Jon Snow! \x1b[0m┃" "┃\x1b[33m 300 \x1b[0m┃\x1b[91m Tyrion \x1b[0m┃\x1b[91m Lannister \x1b[0m┃\x1b[32m 5000 \x1b[0m┃\x1b[36m \x1b[0m┃" "┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫" "┃ ┃ ┃\x1b[47;30m TOTAL \x1b[0m┃\x1b[47;30m 10000 \x1b[0m┃ ┃" "┗━━━━━┻━━━━━━━━━━━━┻━━━━━━━━━━━┻━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛" "Table with Colors in Raw Mode." # FIRST NAME LAST NAME SALARY 1 Arya Stark 3000 20 Jon Snow 2000 You know nothing, Jon Snow! 300 Tyrion Lannister 5000 TOTAL 10000 Table with style 'StyleColoredBright'. # ┃ FIRST NAME ┃ LAST NAME ┃ SALARY ┃ ━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1 ┃ Arya ┃ Stark ┃ 3000 ┃ 20 ┃ Jon ┃ Snow ┃ 2000 ┃ You know nothing, Jon Snow! 300 ┃ Tyrion ┃ Lannister ┃ 5000 ┃ ━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ┃ ┃ TOTAL ┃ 10000 ┃ Table without Borders. ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Divide! ┃ ┣━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ ┃ # ┃ FIRST NAME ┃ LAST NAME ┃ SALARY ┃ ┃ ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ ┃ 1 ┃ Arya ┃ Stark ┃ 3000 ┃ ┃ ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ ┃ 20 ┃ Jon ┃ Snow ┃ 2000 ┃ You know nothing, Jon Snow! ┃ ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ ┃ 300 ┃ Tyrion ┃ Lannister ┃ 5000 ┃ ┃ ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ ┃ ┃ ┃ TOTAL ┃ 10000 ┃ ┃ ┗━━━━━┻━━━━━━━━━━━━┻━━━━━━━━━━━┻━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Table with Borders Everywhere! Unite! # FIRST NAME LAST NAME SALARY 1 Arya Stark 3000 20 Jon Snow 2000 You know nothing, Jon Snow! 300 Tyrion Lannister 5000 TOTAL 10000 (c) No one! [CSV] Unite! [CSV] #,First Name,Last Name,Salary, [CSV] 1,Arya,Stark,3000, [CSV] 20,Jon,Snow,2000,"You know nothing\, Jon Snow!" [CSV] 300,Tyrion,Lannister,5000, [CSV] ,,Total,10000, [CSV] (c) No one! [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML] [HTML]
Unite!
#First NameLast NameSalary 
1AryaStark3000 
20JonSnow2000You know nothing, Jon Snow!
300TyrionLannister5000 
  Total10000 
(c) No one!
[Markdown] # Unite! [Markdown] | # | First Name | Last Name | Salary | | [Markdown] | ---:| --- | --- | ---:| --- | [Markdown] | 1 | Arya | Stark | 3000 | | [Markdown] | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | [Markdown] | 300 | Tyrion | Lannister | 5000 | | [Markdown] | | | Total | 10000 | | [Markdown] _(c) No one!_ ``` Output of `go run cmd/demo-table/demo.go colors`: go-pretty-6.2.4/cmd/demo-table/demo-colors.png000066400000000000000000006171071407250454200212010ustar00rootroot00000000000000PNG  IHDRE$;sBITOtEXtSoftwaregnome-screenshot> IDATxw|F&M6-]Jٳ C(PDPDPD!""eX{-f5ɍWB)IhE}{7w!B!B!B!B!QQQB!B_dd7؟A!B9 K8B!Ba؟A!B9+ B!gB!B 3!B!gB! B!BYa!B!䬰?B!rV؟A!B9+ K$K'DKG!+;B}C Jfݺu111v2DaÆu҅e .YFɿrڵk DTYxm4/_hOQСC۴i#H֬Y\B9!PkӦ?5ZfMppG>}ٳOn?^zŏDT(JP(M6g:tƌ"Ν;O}J4B^A*+%%LJ 狓?55yS<<<*_zl8׷IIIYlفfϞ}"BU%x}! et!HHH[V,7hʕ+eOiʏn߾lbΝ;yyy!?BUqׯ5j0^y<<rss4 _)j˵|B/|~!B!䬰?B!rV؟A!B9+ B!gB!B 3!B!gB! B!BYa!B!h˫J!B!B/!ʃ!B!gB!B 3!B!gB! B!BYa!B!䬰?B!rV؟A!B9+ B!gB!B 3*;BΊi>Hx:^j~srv!!!3gdYkךL&J? yFc||5 1"""i8[bӧʠArybb |}}1c d+W7n\ T$+V>|kB e9KOO߹sѣG gի׹s҄G[9rqEС̙3FcEVNTTTeP _$ɀٳg;T*UTTJR(AAAӦM3gN~嗝;w.tҚ5kj*OOO__ߨ(H$dQQQ2$IH$L2p@50< $]j4rʮ]ΰ`!CX:nZ8~@~WJU@Ea9Yzqѣ`KFh4_bEppBq`Dccc?,;3f8a,˸V!^T ƮXp% @Ptܙ ™7nH.sg2l}OV?xbU!%&&FPxxxo۷ok.Zݸqcj֬9iҤ%K$%%UPRT/i3!!r:uIJRڵ={ܔik)Sk/?BJ8.##Zj999Ю]ol6w%..ΒH&}o.cWnn9sh3f̲e˄Eu8.""bӦMv H$a"""wQQQsNa@&o}B9AAAZ6++ɓCdz^cx$HVX1f̘֭[={*Ћ3qpqzz۷ Ñ^Zxחy;x{{ ccc߿?rH777?su vA!"KߡC__߹sM۷߿(''<Ϗ;֭[^Z Bl6d‰۷dFZNLLB0LBBAڵ[h… .\vڈ"Μ9sҥ &B gR9|pNwYYYGyvz WirgBիWo„ 77oTjȑ#ժUVZGV ۷oƎ 7C////u07oޜ;w&GGG/Y$::'N duy7mڴo>!I-h42ĉ.]*r!?H$jS}gΜ1OصkWZGu:ݲe˦NP!@E!\AOE&$@/< d!>M,@"%d9y;cpggM!C,2BɥRΤ 7B!rV؟A!B9+ B!gB!B 3!B!gB! B!BYa!B!䬰?B!rV_TTTƁB!B gB< * oB!rV؟A!B9+ B!gB!B 3!B!gB! B!BY:x!3ڸas iTBBU%j(fN>B!rV؟A!B9oP(u$IW.)!8`n=5~B/SɋE|Hyw횇Le^ѲƠٽzFo޼Q ddno`r;E~(3S)'I+r.؟A~I/E"NЙaT*}&QV,(3q0JRNMڵ%c9WW]hAtv<(3zwmp |EtkV?ln=FIgg Oto 99d" /ge.\!rP)cVÓaclr+~ ToX(+r,Enae. Vn\ۼZuZvrY!rNzn x?&OBgvBg&f//:7GHΥrs>Xr)0Pۼӹ^䒘Tf//[ss\c.A;ownYʓh8q/P Hyxsqyj^cPN"!MF H%CGMQլXZ4-I~h M;snC*d]d"L&+n'=P>`<=YuuVfQSKoR9DggSSkMº=d.mxl&xYhڴ݈W=L@ą<1o]0nntnǁÆ7ް4Y7(OmBj}%-XXMzۙ-,PE df_ҘOi4.rw7`Be)A,߀Gz=IoдhIeի#ڴeC7F,CXRDټT*_׶u[:7? !ѴQ_-[vyERq40vjvVJMS8sZgf(k*qͪ];mgDJoZNBUE CǮDt^֓&#Ss~p%?VqIש0؟ALIe0ԭ'sjݝEY Kď;&~b2[[fOOb:kp]D<3ݍ(3SʓU((iÃ'qFl[ ycnD=3~oBU׏wn3U('\a<xфM~SBoSL "+RJwlvwx>^ t7f//2F*P9jJeϳdt?|Py`ϺI #* ,'2?`z}d2F\yI<ljř_ޤK0T:kiGH&lʛ(#]z&dȓdN^ ~,Ji4Z/A2~b~@^OM1;3 ۵'uoMtvF#a6 IL^ۣII4k( C elgT-YϺik)XF} !Bu q{wR9 c=N"^_c߯?ܹX}7<@ڰp]pʠw?t yE瞑g1{㿲fL wfd'vq| ICoiJ&ar:} OfavmWߤj6<@FT`=`Suz$tyRZ. NLtN3{ x x^OӬ`JWb1/fau؀b y;=<N&cr`:曕ڨ/ʺz=-'z g(ERRE38Su!\^ ƙpd2 inqVKLċżaM8Gd,.B8"pR1߬F}I鄣U+P]Q~ϠQ9E҈|QjOw )MXx~OF`'# zR/zmԗH PEl_;B!rV؟A!B9+T WʊE!x^r6w" =m*wwfl.>{oXkCaI7l׎fƍ/)Q~@C,i0vD@.j#qS5%_j5}/D ի )wG_"j_EiM=O3kKEiĻE dL'Lhl}N"TvZ/;Pv]1K~J 24lӦ#JPA^6ӫ8sځBR~TZӧ@vx#xksKR/ rL.xoI2W]fX$IN-NO#["kڲQq`R>(Ţf}7qJ6C'׸+tvV44]GeK-%yk_}S_}A痴}f}_.wX]Z$`^vڡDfko~⮔4~9#C*$ı2x?Kv(N;zw$dNfMYo.!y A)䠁ÏD jkv~;y05qܱ@9q~v| ?s IDATm kBeś*tJ0`{ȢTvZ/;Pv]I/onnjƂ@j@1{xrr+I`)e"ɪmѪmѩsxRk|0 w,]={}7x^9!$ON?|=Tg=Qx^֫SUzcV+!m`0|}@y)B\nS/VmN,7hh3I8#L2(I~yFM u]/ Cy % C0 `Ag}|8L޲#јUuN>OjUzxj;Xx찕vosN"uDCDYT]%$%%<ǁyKN $) O6lgd,sV|{(O(s;qJ_P lbcK `[rV㩪`^Vڮ›C?c'w.8n?~<ϲv%q`6FcYU!'U ՞Dp~DU1zd2YƳ"#l1$zċD$IIN˺)>ɓ .Bf@4TӲufKَzڏlWWOpz9Uf@ZZiNFxzR]" L&gӹXᴛ۷YTR] /oTJJ%TR]"Cò#lSUFJOq?]!*.<aVJs0J+p.{OEG@8ִi %JyTE焉kF$1j(F~\xVe+Uxv*1œ3;cq w2tܝ;ڿ3O/on4Q4Y:ݝl(^xTvN;@Iڍۧ oW%!T8De20Cp}N*5VӻnO~iSS4ʀe)gjؿGtaW.[II~咦E+ݟ۠?ulߔ+W_؊Gs{llZ/;%i7cWWx*QUyDEEEFF"ɑ+G]RnZxss!q ;Xz HšvC%t_XF; aqOh筥 " !2 :5<?|/K?eauϻQvpB)B!BY9}rYeBȊ BybB!T B!BYa!B!䬪Bx*+TSBbbeGBE$Uv/Xs@UI֟g/jRRK_N0̲z[S?21;=];a0.Z;:|; /:|ᣆ3råIGc`琉Xx__˩zhZ_?};fꅗvYQt vRԭׯkkKZa]&P=:DӖQÇ*fuwsrᣫ"8{uVn/G)u'M>ig-›師w~,mB۲׳RiKֵLK_T31z,JΣG\aD"5cZ#*7S߭[w􋟯'?JsB4gyr* ٹhWOͶi;=%5rUa.]۲u;w@՞8uh4Z]aw0L{W":E"Hjnf|8=cw5؉iiXmYDvv *1~!x]&թc+W 0v̨j6nCc#xof[wXj={t# M?ڤ1MBZzu\.ЮD"dk_գ{@GhѼ#Lfl޳ 4nةC{?__G,BRjт]qsz ǎlڸ>l忯~0Qot,>:ΜۊU'?z4ṁɏ$ɢ%K 3g1OvΝ{oʧNʔ&](JxqBSHssi:[`= a򣉓4Uxc\rYӦq+Wڵi wIKKt q%M!.7-777,4ԒئUq540 `ܯܰAG&zAdтuk:ʔq~s²eq"y6|} :uj{^Q}tSTBڴ+W$ЦMO9 z,)I%M!T`9ohxaa7o9x<=WϞ;vkb.^ W6nj/ 5rUިn豝;v1-Abcn޺uiS&o_|5>lۦUU9޺}AB =#C馤iƭk׭eɢcgeg,[to/ʮBXFtԫgXKz7n^rڔ۫W+޼۷ի?hZF?\F!wkO?~[{Hpp|p~;`=/\3*bOT~9zut12x{ C{^Yyx(Ǽ[p^OVpў_E"L&MHLp16/OUhghW/E$?~-Y F37i |Ni}}| ??_"i'lݱQ[oj'%D"{'Vkf3}6y>''x(Y:BҥKK~H5E>dHԴ˗{t{eǮ]-~i޶MIuNy"kQ&'/aɶ{~Z؉ib4V^Z7?y Nd6ƚ>z.myzFRg[wt9 4 dG=kF`@9vb}I_ܔ-r9 _͝tcIIGJ/$tZꇄ(ݔB0V{N298&-T^5<*sc@,׭#"7nE"(i:BGDgΞDtлgKփ[WM&cvH2t왇+2Uڸy˙ 7Wä́7#ݽ{?#3h4.}KG4nwf5k԰3K$1صwt³...]WXO-cӋ; K32||7l(":uGh4 ѩT-k7ڴn !\$N o//D2v̨ 7;ɉ;WPZts+bl =wjVJ!StP9~/sffMnJ{G[d`xv!OHW$j;7<{l l0Xl~}4ݼ9B*lbzt0_Vv8!rqLrJS1~-!ٕBȣ!BgB!B 3Vaʎqu}[Kߨx;]*ѣU (7lXd:A֪UQOзOFʸc͝:ա#5r8<;y]9N^í[w[ q NJhG7 m\F;=e6F=wd4լU}[~9 ϝ1۵i[D!Ϝ=h"h߮#%M/K#}is?제{g0 7o}k qvƭM?T ;^"͚5R;Z^p5ТyXYW9LӒy؊\K󽖸H\$/P㹝"QxxgDN^ v^VF)Vq֝Nj2Of/7լQi8ڵm9h%?-제@>=i苙si8_g&R/rk@윜o9^".efgeڴT*p1+bGLxz?b$U7:~b|c/?ZjԻ*H\8-;?ߟ_Zӟ=sɬlK+4yGNܺΉ\vmbMtm?nm7蠟gΓ}~bTRw(r7Lu7 W7hێ=w/3c޾}zdt@VW jW\+ׄ{;)Qw3ͻfffHN7n~Ru޹1{df9vKWvv ]8vP͊Bصkڵ4mNz̈́wms IDATԴwc%y޽ sνj>}ҥp֝pgWB}* EQjrx~Vxp񌌬Ǖ4݁x6ٳS:vh#\[NN:--s鯿sEisN /D@\ܵX$ccdf=\v&&!b/r彝|hҤQxxqM4 kKиQ)LYO>QHtS*6m~ݻGg=E.˲nK6 Z]g*jE  |VݽmDϛO-88mڵ4n`媵?.+<(u$IVxɪ}N2mVoDzvnAk޼yΜyQxxhVVNPPJU[1ൾP)w?1:zW>\5q%JW<d?{&vX)!1iFZN_H,zk{o"ܻWQUVcO 5nj~ߨԹE)زyۧS'֮]&Ntޅ*2/UZ*uquU+<;gV=[?=d*禤e3%I>uC~g@}9kq$wԾjd!?WW f;忝iE϶v/1$i^Z@LeOO*VU}OJѲ,[;p޷_+I,Qzvvlڼի[{fnjլ1iҸ5/|@yo'/l2~Sڵ֯|şEʩ]̯,SǶOfIIɿZfJww?ݻwρ~ZUR*R| |N_V/h-:<{6ٲ%ٱ3f^e-'~DxzR/<|AxۮmKa[N·z T E7f_h/svnj wܿnsyb2 ,n֬]ۖ0 2Zn/+}J"ƒ;L& oݾۭkg!ߋw7護y/5Mq ԩEQPz`ۨ^9S|e_﵋ž>#\\PM{xdaýR&#'W 9rbz@CDŽ_$%z,J`Yv}6ca3,m[v~pJPP __LHvI*N6bSM4E]]'edg(%^ )^C4bXN_V";veYHz\6bWh7&IK픠LK7Lg ̜DF;Ä4Z=ƍ!W >>9\vVwXwB"b/QĘbn5Qcb/&znb,&&cFQcGQA@,, [2cpE`GgΙg9jUSjS9م$.]]DDy}/65ZC߾c9AZcUUK.Rfs祧4~N$I5Ψ97q8$}['/-]Hl=ܙ zEQ\ti3,❼MKm6VԚ-mޟzElpÐqq!o"aY"6gr\K.( ϿxxWk>^÷^j9{mxESmuj:ul~w[q}_n?Ӷa~q M[¹N'r-\pCɽxDDxJJO>R:M7?vů'4źg|adT.$4o__8owWd$en ص$1 AÇ >}//8q"kio|aqiY#cܒԾXƍh|a^[AqF8uTvjjStI Kӟ7=[PPL&s?6oں_xsǎ 2bk;|㽽_cuupp`aԊ9b,+y{LF!:uJyǥv^}./m{A[UqFaŒG㩱׶"s+* ,zRRLrzG|fАEkM#c|n(u=k=m싎l:t~RZZ`b%/׾0СKʟx9^{Qå-k{VO ?'/_ƛ#Lo\\!ӗ%L6h`<Ͽ>2eλw栁IxiSM嬬4괚?t;?ь.35A[{ͺ؝;q\a|k}iԙٰ⧖<9pkcd(*<5>^W_([߃ _w]%%e^iࡌ}r>rС^=j]wޮˁo8|ۼ=M e`J M7N.3Z[{ȘFZ2oM7m4m4I{DTBCRrVsV.1Წ6C5n(R]F7>]m(Mji_<&Sm@ΗOJD6riqO|-VR! %?t/EQ\3?8?!!NlG~^[;{m+)ɗ՗]6r r#͜+T34m6ܪFi_L,Z-PEMA@t$F7>]m 9AMji_<灁/A m@&0zxj5j竻ww}Ix띵yyRdkV/qJp\v7<|v/O+r4no>yBivh^/%+qO%vq}̡oV̧qWz?6OB.U^nصko||_fu|7/m+$cdacƌ?!9+.>ǥ^E!{ࡣ2=fҧqN8.:iB{#G2xŞ/,zn߶()wZNП" F(59˃Bk>aDcuּj]!B!:/ B!:+\w$o=n7Wb[B3f3GcBAM~{w/g(PwE}ٸ>ǟml|bC'r¯+:ovs,韖Ҧkiv۹kWZjjPg;={%ԔWVO{]k,¼9L7֝qxxIөcEnڲY}e㾛={Q7[+g-["ûҩ6}lcOZZƎs<3djR]׌/33OHzI^w>+GWhpWli!;.8D^\W\PP}c2 8LoXDZEY/_l ܳy_|5pZ]l{N޽-nrgv=mZ+iwWI/~JK7o'<@ *8BW'g^TD bÇ OS[jL"2BJ}ӯx$)4MdZ2OkݻDŽ*&'ܫg"˱ӯo 3OD!onT/Oq/dJ۽ Ce80tĞҢZuxښliu E6lysINMRq@VV>鈋33SUc--"%HprR\OwH{A*+##"BCN79Gr@P``}`0d\2VT*0TVJ!ehfA N:պց"#"RSy {6d\&>sƽhЀHׇeܳ"N7d Vs!wy<}^mMә-fG^g9uH:}iRqN~>]$Z,_?AO" ~v0T'N x\.7N^K]&֬_Ŀ>|cIi<[\R &ŚCG>?039g]h_2Ofͽo /qW<,l^u}xsΞ/XS߿^^?,,*.5?+VٻoIk C}Ï/,2[,?vPvy5q={ڼ\ p<3sİ/VTT;)BKP~$Ar$$(Ξ I{BO`: L cON<'RTsҌÆ o¬zqEG@``y-70޽z_j|aCK=XĞ/o͏!50=W_ճg||4RS<Žl֬'-\ 8rA 0|ؐ}1:v \=}^mM437M!ILKMݷtRIliʏߓ1[)@4 (J^B|qdAh6N L4-[z'&IKϜqm wN=$8|5|dx-Ͽʬf4e2 o~狊{ zlٺs59_J@>Ik?xWfl߱#_= 9y'm7ߜ?׎I֬4yZe4M;]>x:xR4gW-2VW<ߢxhHH;TW+匎] >Y Apu?!ꝷ{REl6N$,n'O8 ?[Qa&W߬^6nh_k]|h{O?[ь}d11?s'N\2Ow" Ͻ}S7`q'֬ۤ[w^S ?#k?}ʬϫuÆr'Λ_:pPXhh㸤^s#UpPEQ-WVUC^T*[,XX"NJV+RǸp85[,˟z"4$DR:ۧ u%ө߹CkyHihZP 3g~9U[C\)y˨z=3mL>8QQZ?;wd=?l<矋c˲uutmOjQ}?lȠyzȠA,6J ^ Z㯟8>:*"!>}/} =jzkz`!4MSN^ EKS֟!LMny¶@ʿ5i"ѯ/"BXJii[n;;f{QLn4UJCF *y $ER,=w݇o}GVv{zS?`Mhh˯u۝3>^p8 QPUe(`0̾N!^~ӭ[rİ%eV5,4ֲ/ri)(L۹.u)ssvli: hSRfޣk",՞~,LyEE@b IDAT_T* L߹Ob|$4,ja?]a= 6d'̛sτIJ"~բf˜5wAef!)FC<gE C}th4J^}ǎ "00v BHihE!DCr399Dx80)Gt{ktA5; 1155&4V c[g;XH֟/,B̻jʗ~ i˳^~DFD<74{#V,CUgS%e2Wv? +;;2"R08Bu(; cJ Xh$8<臟~fs#V}P`4LƄOT,}fɃ>!=[۽Qw$IWTf{k9,y;fH[i_eE/ވ!4PJ].s9M_ }\, -#tuϏEǟ#Gjk~"Rsu*;/tn =GÆ v:>NG-:zoz_:ǟbv%]wr8﾿޿ˏeZNR%aCɬ,cnP*cFz5ұhq@/? ~qЀy3ݞWPYUt:_;nҷ4]kԎVS?K]>@ɪo)`q?\.7_Z;MqI#G^tty= @ ɤN oAoH,fs]][b קL\;z4] sI!-OFHuFb q!!rys)}K]t}&"}=xn'ƌnI #%%CMQSc#$o<[0[vn8F< vی DgSi<4E]՗M3j;&d+yF_3rK:Nt޽zf䃋׌6jH{dko>svp`f{`+k+_H-{Z_;zTaQ rǷ.c21\s~R\y /~j i#A~S&a'J1t+o}⥗c|e+_r,|Ex;z=/ 00RrzOmKz\!@Vr&={yӹg?y7W=Ȣ/7|b4Ukw׬J_Ͽ?K3nAKRvkXo[no]SM6M6{ASF#Y_.;@@5~$ =֬iz^,([ q=iz1]:b]l1 @"R>iZ- wuXV㛖nwV0Pt|i_~>8 r9d OF]IҁQ8[G &VKy>{OuW)ZPYv=4NGDMMmFyFV7xuh4_T$eٮ0PyeӸ>?qe17<[Q5XfTm6,V{E ^z*8c~ZH0 ;3_>vՐ~8Bw\&/kcM&OpϽ5\l˕x ZE\,[}cW [Gj#GFFoݾW.  "X7q8\SA!tZП׶+{_ 9vwqBuZ6 |{/B!r[<B!BW3!B 3W1Q$Z4$`*kB@lh.BcxHr4#8h`ŨQ5z<v$xKz,KXV{qh>+7.2M+ҦU# [xӍ CA+2'{!:3=h|@K;s*'ɬޅѤ(FTe3,%w~tJQ^k NF;5G/y $,2+>Q$O^n2 a27@ԎTdVK޴$EE&yI/ѝ#YWՀ!"Չw!ٳ3LN */=|pd͜J-GNspojҳ z&wr$9eXQ/%$#AB')(<02ZƱqI+Yx:`O`)GWx, D) HB$Io Uwl^dqee)v:rr`EmϞ7L5;f2\:=,,qBS#DRQ$],p:SrAW)TKYUj-+: @LEc ⹱}r:.\8BW'\u*GX(SaaRW*ˤQTYwlbQI%ޱCSk,+Zå%7UP|^rVy * (b qZ؟0fҙ] uuĀܜ,]ǣm6^IRQUqx:;~!Vl*qɌj){WIxd"0"-KruXz"y\xG]zS2Izn2r9XԅEնPO9;v^;S!9_B!dX3B_7d,tڣ e,݉ɼ,$t1iN*J8WP,V6uvgtygefs?|SRRۖ pFaJJQvVBKR*CF2jHR dIXD-eQ9,#;'-(ȕ;xG#8XYYIlZ $+ Cx|( Ǐwt-(EUҥՊ#PXuz\p:'Ic@P#tgB!BU r_B!RWB5 B!:+ B!:+|Ru>&Mի[+ ^t.8Fsĉ˦{ۗۺ!U ,Xe˖"eP`aÆs}G%%%#FʪaΜʉ'ȑ#OT*-g}vjE9ݻ>(UUUYYY~lSL ?3dȐcǎyZG۝Nxxk aͻvj]Çob#oϴibcc}Ϟ=3fp//|ѣ6lƌ?ׯ_CI̞={Μ9j̙˻{˗_[Bv;1{ymܸq̙y_->OJJʶmX]vٳG@RRqjO> iiiF*Zm5MFFFyy91{_zyԩ˗JjujjBaaa999III111gΜ)((TItttZZZeeeNNTH"AJKK3gz? wPUUװC5 gΜ0^e7BaKq\FFƚ5k͛c* venǏ$B33>}zԩKD`iiiE1--m!!!t:,XгgO)R|IwMѬX"== ->>W^4hPRR+2`>==}֬YK.?dɒHOÇ?ZvȐ!-j"F}I~&MjI&=<Ϗ7nڵR$-JMM׿գGzB vGJv9j %%eٲe񱱱K.:tEFF.[LbguM6utPGQ… 7lC5:&$$|Gu]<U5w7 ÇNOO_~/by&L 9r/<%I6m侜<E[.11QO:uӦME{ƍo?4]S4_>!!Y~tVZtR뮻lv#ؾxnwׯ_߰ofJJ 4Vx oO}݁6;Bmݾ~x_zO>ɲ /0e"Ba߶mZ-1b^2h4qqqjz̘1cƌ 4M7֭[H{dO{O_[[qFj/剈 I=Ș/Y|W\y܋TY/D$EQ ½tΝχn4bۥ&8h6+ӧҥK?# V^}e˖8p ryӱ*++O:5f̘۷[KAAA q7ht;$$f=Aӗ6\<:N@p8B'Dm0 ֭駟x(_ŋϔ}'BSv|nw9;wBXx>+=߭[73ͮWXbܹk׮T)oډL&[bEg+'L@s礥F1$$w ø \>cƌ?{cs0$+ƍΝ+W ,++;~9sIO~ܹ2iizO1Lٳg&IR~M߫gϞm}H6,++W_kΟ?w߭QT~WT/l&Irƍ& yx`͚5,VVV[5zO{J{O<ڵk].(=!!!}pw̙+Vx,3f̘ٳgr (fҥK_BvFyZ֭۷o ofŊz~޽>n󲲲7|'())9uꔯB~Ϡvhܣ4j^5k֝w{ 4|rэ$ɀflܓ hO .o$66vŊ ߻j*5Bؾ펤iCJeHHH(\}kݾoެVEw߾}u:^;v죏>{,J4iVϖy*OiVFdcǎy~ذaކ! lw$M .t7Om"B3oѣGsbŊڎ.N/5jذa.ܹs< 6-!tY Թ!/!ΧB!?B!ꬰ?B!ꬰ?B!ꬰ?B!ꬰ?B!ꬰ?B!?!B!ԉL6 j ؾ j 8&B!B B!B B!B B!B B!B B!B B!B}Ozڮ!:3yÌ+!PWڜtSB!PgB!PgՂjkA( Z Dad^@vC9Os(߀ \c=wY[#jeZ\-V"u :*rDEXo}5-kQ5xUc\#)a-cz#ٌӿ:zI&MPw$! "۔E`Ң QG2eHA55Dp>q qiX>Yޛ [If[W!'3;&8 zEp^(5۬EGATI}&BZd8 )CuV |@򄦙/=FD`)ugDSGuE?:::- qs Mpڭ!o/߳stI=Bs! S+|dV}GfZ *(&Z\W~z(i#Vr&w'AC6oǾf-׵zk:*sALuT38Uy"ԛ@:,]Hm3mFedPӦGs `I$<X ]RgV{VmLBh(RX[ȳVzD) } 0JNpu&OzUT h<ګnH܃Vrv% DQYXgn$F\guT)>w ardތ>ԕgZP$!Liu3P_JVE"9^~e.k'B(5ABd4a"F3+ΰ*F OU8YpuAB6\!uYbJNU9jm' F2ʐv>M2wZ ZF*;T^ΗACC$O$Ő%n9w)C{ʃKrՖRr( AҪ2]nqՔЪUdJGU9huX :a=ԗwN5RH"[ !'؟AVCxj"G<8.8m*I1ͤ請Q}j3I+'$-u-QtY! IDATɚءǿW5^FM&vVk!qڸ(eA:4fNK޵޺4< i SV[Q".n8(EEI2*u?$Y%V*ZOm<27(fKGcf[ Fl}e3]xM[fnC.3 )F&Eherx{giU9Sa @L%N;IA4UG7 Vr\R}js+;AT64$g֗!u0b"$@Ki8&{*OpuhC FNP ׈<+8RXQ EhOyH@.`#.gkrv0pFcPWv*dbӊ"  U (=^L)†βo!B< =F)tI! LZ+]9KELC+ ZJ A"Ϻ<HKR"(A"APA5yZ)ӄG$i9AtTu[PE5emt[A1؁AҺ0su=Wh= >S$q53u$%'4+8{m@qP2241(&f]xDB sBwDZ(-s 5Ş٪Izwk\6VO @H/twZA7VDEZ#V HoN㹲GطS!PWŨ!]V&J]͙.2"dJ )B$=lȹ@$DZ yZ/sԺ#*<ܕ+@r5A(B}IkA7s JFA5ΦA2ʃ{D~4xdA˂Cu* gPU$p9B: S\hB‡%-Ǿe`4RR@ZfeD; b!8ٺj\;mIJ=p6cj?/kM6~% NWB5!kQtɃ{ȃ40mÆS`U{ɇdR'' &Ӆu|pN3-׉<>/DAqt7*H"Qr(pDV7 \_RN s`9O{ɤ+J:9{zJ@2JJHʔ$-'(JbIMOND(V)=g7Օꐐۀ\%mU|=>q9\ CyD|} "tf˃b ! ;3ڐ\?-F`$vs' t/YmV>dHPʈdJ* <wr<%QUU}C\hu"$MQE .P5__Q_jB9i_>b(/{@-2 IQrOYìf hQ@/2b}+ӄyg((OOx l(I* bIݾ! S:/ΆQ9{|̅b\'SD`Ҕ8C}]p /Zeٰc@4ھCOB1.ۓO|OFB2JiJ_꡾LIZBm A!BuV->sۮ!: ࣢_|Bu !>B!ꬰ?B!ꬰ?~ٷ߭!DTGUξnGͪΟ,ړ6ya>Z^7\Њ q =_dx$H >sVg$-g,FQސwg]И^#" m?s5&pչV̍F]vƢ-6Q}٥eCZqn0Nl] M)EΕv.k)9ȹ C3j cdH.q/ǁ/%I{)Iyvh^^CK[UH:(sg`,<Żaqyx!K3/5՜6nk鉺̋I7E=G n0=2o@k}mk)iS] K_ 粈$3NRb^nς(iR+/K:{ }}bhJPzO6k1)㇠53)&AZYU_xiā}ŁUBי1JV?].P:4зȷ C@]-NlM|uRIS/چ 2[gݦ4<6zQ%pwQt괧HL_^Ӵ2OWȍcTYS! ̈́BGGZٺdşj%sKv0?]'|BmOF!{p;?A +Z˝" ;m[p{s/yuW]/L9#ȼ=+c]^N7`Xp٬mB{M:i~0R.P:wyiAWǁeVF ȋ4"m]eз<ٹم wvG]y_ \kk@yyw,VQf= B.^1u+13Ƴ4SKS/O zuI%ٱyN{Zː=0zKpm=+nⅥ)%|J^ʳqhH>mv𬞙^V)-̸}ԭmXÉvt ʺwl{0 9.?ZB/ʋI55s:ӛc0؊]|G?\&aȞC2bOI_g{vֶn^}3@ a%@L).KZxkGk=z9~2tYт;r1]ӵ1Ί)rJJ%U'qӏQTT24z.q?Zgi~0P.#ԉu+UIg)ώ5x gr/QW'ئT+1Vƌܸ:cF{ rm[@(++ώ՟5d ZPwTrCz/ ( L2d%Rv!3oZ94;}MLӔ![۹d^,O#|[W@CWWJS CV?*'}z@ B T2 <.N-̽' 8wwgm:6$meie=vEӨshM̽GZ;6)PL>+QYFU]0=1I]tqڍ?};5{'.)bc9_j\+Qas)7 5% R$902uw0;B^_=Li cl!,?)~8+D 'Ve-C|G#Adi+H#OY[Q$˸j:ԕws4풵{ro,xJ,xhQ#-0O9ڼPN]x+C'k~`(]20oW^d'z4ܿ ']9K,7 Cc5F!X#kM:WwgfȈ5 3hϿB?@rY~C ^@6%bs?q,jj^`0U-RMF"} Cǯ17hϿB?@rY~C ^@;4K|il[8?868;zưwEdĂh~2@ ^ 3&,lNnU;':tdue'V].#~0o8@ @ H >(6@ u h O@4}@ @TP@ @TZCFmhGyf㬬@  $ۄiŦD3GgŘat5iW&LeY)~ҥ2!_|Uv#JW}&Mzc7]tiҔx DA#'t}Yki}p8wgLVxgk>}^a6>x޽?~iP~ |+5gʆvJbU+_~Kܳ ܅K~ϖjUky-|>/ ?gvAa?pw'W`cxfv`ё֪vkójek~oN{Яϴ|*v[ Y?pwsMII۸yƯˏ;N{ʇK`fċI|q 9jAINL&?u,M?]m7DNw?WTdqw:s39 9!}$IK._z~WwueeeccKېc^mݻgBbw;ӧzzyzk۾̻_jf) On;r8ƌz90lllzɢ";9;9w2h<=nsBTs/kZV{W7:EK~ӟ5U,\>J>_ "#+B۳&swNn~Nnރ^8<x1iù ƽ26=xv-}<~Ӧ4 rg+ ?JIKȜhך0cq@]!'&68ݹqvaQUΐgYT&o=2^BbxX4}/!G񓅅w%4m޼%B g@>Y5szrJ݄ďͯވ'KjZ{ݻui5>Rt)iN)#Eb(%nݴ~;K(2Iwrtl!zѿo?i5C !8]wR8qԈC&@d7n& IDAT"HHLl<1l6?ҕ{~]kAAakmވ'K`/fw7nn5>bxْA?x'8;9IeRNkf]뇭A4weSREBӀ~}n܊|:N<Ç^vC*K $IZY geމ*J5-x?l%Ir9KIT[ 5C{ s};[SgVV&0n?g>~ :NGier}EN+J2Uo!z/ˋ]Խkgkkk&DǪɇᅬݳX,~}:sյZBl)B$q~ht: 3[nߺyWBވ'Dt ۴nw?lMP*{qcFiC"p'oBM՛dDѲϜp!/? @՞xCM^NҹDtxÆ/>߸y{[1 ciR4699 G *Hiu=־]?ݳ{:P$AޟT*_WF ؁aD #~ܾ۴H7c'MOrrt:] 8Q6oaݺ`vJjzee%0`MY&'7wM;yWW55RCQ#'=x8fk7n֙jZClH,.#Gݽw0;;B4xQi \tK=Og $mwDP*UZe:vbVv+WFr:-!bhu:o`ȟK- M[-Ac\.ƍqILsalŪܼ<kؐG9>k4L_QQKޟ ݧw*Dr5NˣdLgH-~0֧mvٳoJ|IʻtFQkڅkZ0UG ^LZxB۵:x`VVvVvuVwgؐR.-h4\yJ&A̚3_sc't:]ɩ8F{e0<J X/XrП7o@yt߁CY 3KJj;wO?<@m|cx<<~4؊m\.;WδF7Ir6o{yPn-*.vvv m׎%FG~P5 r/TKe1ݺv[[ce?urtxL!tŤ9wb/x|~>'NaC}DT7W)M5gDR>_ĝ1yM[ A٭Z~|mpP@t[zֻgφ}ْ>bw+Xbo6tR8UF~ԙ$Ґ#}^Jx!9놠iS#__Po{mޘigg{¥BF} /(J",]~~`rJ׶|NV_~ʵk CNnklYVvv+Xb%??ys9B`eu^|۵ZݱN>k@T `ݿ ^x Lag &tW:Mau0 0Hm۾̘VuQTrg':3lr 06jY 2@@VU Z&IՄ,.) `Q*U4MUl@>-mgLLNR8Օ1a!RfNt;vT]BaQqqhDB! bHTj xuiVQ&W_E 8Q=y: H}v Y4`乹~NLZ&)?q̞]?j%Ԏ: 6EJ~0oq j?h V!c@ <7ωvC zs;5*Z2&LVC5NAGJe10MlEs[@ /(d@ xA@ RAEg_sr i陆>~QS:8ztRhӦVO5MBҞ۷y ä8TGiU.]z9U bnfڿhf!%%[!g7-coP[h6^OF}ߡ۷JUn]^BPP gc8 N]w[+ Gw҉f=~ss ۨc'%R _yj5U!u?p[]LOxxw`ѧZr#'bb4[KY*Ʀ[Ώ%Kd(З:;94+Fm1XYg9]',)*[G{I^j##0w2AA;~x;&S+lQ[<'+;g8o{C|cN 8-tx⠾űa9v}RUeY"%%m;t>l_Y1/x{.h9@ǍimT `!.g8b3Ǐރ8a|-dADaIܽ{l~G> ;^lr?xK{p:5j4׵ZV-jqOOĠI ZeDqfՒɏNtk%v;]}9iCWV*n݊tZiY^$$.^MIIA7u%9'wޟFPoiEEӔed|"6loL mcnjG>ı.N.΁=in*cǎ<~ cF UY$TUk5(\z/!]Ić7n@rrH$ >JN}ZvL|ܝ'MgiivVv[ KMs] 6zXYiR. Y]׳B"hdd89} R2GxxLӴw uuu>y\FfV^n~^XP=f6D_XX廝Ɔ-~cGFF4ݡC0G]m-xߧۛEbaXX+_4o};x`YMQT۶ lؒ5T4׵Rp֟Ѫ1G}F˷pBV8xԸŷT+|k;m=jhھ]|~;"7`%8{yy\|ur&NxR]:Gye~Sm?򛯟Orr*ܼٱTljB*+utPZV&uϗ/^r)M&qcG~uGjZKC$ FyY7n{I߷5v=$V1t)<|'1xIg}\DG ǂR\|9>88EU8)K[єCp<]]/^~#%<=؟g_f&ǂR\*g?Cilu Ze;<((g0RAT}2?{R`V[OcדVdϦpg8N(B=a+Glٺ\>c{+yK}];ۊEB3Jݔ`)rZ<pΣGUJKl|0K`& KKJL6)!!)q;Ks @A8Q蓚>xpƮ'ϟiÝ4U<.0>iNxe҅\]j뺴\*Qj#4z&vnA===$A p1#A4WTnn.r\_ZmIiKCo@Phcc!a8qmB #4ʶtp{}zKd/K63$Tl0 ~9_A}ɔ_PTG^xws>)WS6v=1~(JG5ku"ѣ7kZ3lvi^ ~';VaXZдNy:E9S^!|m~~P$Itt/lH-}{HOϔ+ 57f?1aBBbdDÔ e[߬z_?WZ'Z3[T\L6a_/fչj7](ykʫS6v=1~rӰgQ#T+enۮgHR\lp;۟)ʸ,/ 4$GU)utlF >r\R14sfL۶Um\]]:th/״ʿ'F~b$6V)i(?ChC/*SPXz0b쬜2FǟÇ6OFWHHK@ќ;w>\R/Xލ1gۥ޾Tjo݊5##\~幹$4K<.5uރF{>r XKV Ǟg\ 9<FA7ñѣ | Ǻwa2޿`״ƎojCr//֞ؿ4!s 7̌{O|p?QXXh?Z%:8^c}<\W;v<"9_\}ꓛVUT6|׽[[ZV`Od׮Qfo=5B>׭(Je6~g|~!ed> go24Fuww7OǦ͘jo݊sws]vNG]x~0tܳy}3`VKhw;}777R9]O\ٷcX݋c$Nt'xADx][arjml<.A&rz5yĐNa˖?cin&dbe_UM?aIh%(ah#RflJtB:VTVtTL**+1:M5>hTjk2UD"L14_4 !+&\ h3"p$?1y6?&<kk*7 {#5>h< k2U깝\V3~Aru] IDATZRZ~6Jڲ󫽽=M}cUk_b͆UHOd~Ïɭ ڥƾVJj{_s"A:|>u/Tzb^ք~)3Ϭ\n4v=1Ro'4VSPPt5?6Aܺp<=}Qu'oLgnM.5u1*Zu,xB T?NY̋5ߚ/@R:9xfh7뉑z[g?pIs>=32SյKNɩED_~Wܚ]j1z/D0N]u;Kx{#8rt|%@ ,NZzƍzڥs'FdklS򑖍M xx5 5 Mam;6=hW':~p8٥X`^+ xzm&MVXfo{դ"l'NTU{w׫W0?+#CZk9[oUIA}cs:ʲ&,[QjZNG]ieIGW݆`x<MxF5E /ov+*UߞᒥƯ*.-aċe3b_/8x8GJZ@IbH7 ǹ\p$WTTMHw)}ېĤwh.0$ĤX(ryE\|RĊcc:Xo[3_Pp?fC=]P F&Ho޾hBb$ puqa7Ie1ryEΝ:_\& E|>}Lʚ׮moSuQ\4"EC۟,O9SH@S+[ &AӴFCt:F666a1qXQ:;9&=|ھ_6]hvPTT(?BȈ@w'DEK稬'IIqsu I9 n\t#~wҕ>JNoBiԴBvX$9J(jl"L.ST5'Manּ6q?1UG ^L,H,K0$77 r(ed ߴdB\ vEcm5oǿ77/DR~+&nϯhSҦ;ڍ[ 3V{X|EjZG~~yK4]QJZZFFE/_eH7ⷷf$'W*oͺogظkׯ8wۅEU~"g[p+φWc#u:9v"iУ[W»h6UoV N x>l ( SƫG`˳o-Z^8wnO+`iu5s7zc$w5p;;ۮ#|}#g=w mݺvf??iU+!i??o֭Փ ːno?mC,}ѢdQ={i:*Sn]\?yH(͆l|56"!{|yP7WWVqcXa/w qT G O,VUxz(ۧ`{PUq6NȊޕjhNom $I2awnp`3QrǾ;cL0|??g$eORޘÇ@T7nv'Om ?6'NnY]o m}o9U^afN{&>k䔔 -_=MfL}otoh}O[N竱op8j?dqΝ"1yo{nZ?٥eeE;9:6AN _d7Te@ŔV {k'dpN]YV{ԩCMNɩE֐?~.ػ`Ii)gd~q̩o?E?N@:e*z׬9(|ʵR.\4d&;;8RUth4Ǐٟ'X&$I++㬬;2YEiFPL*onnގ]jX$R*VA2<%5]^Qyٓgd|Ymߺi3ywg|tV&W,^(T*S)њ`(,OVE8wmӆy1 #㱻oZBl)B$qn*I~f.Q˗~%*JVd%Je"0(_hc/ K\eA~wp[߾a-wpR$.]W\RT(|zxX 8m}#zG K |VYy}<=9z}*.*&틊< j)q)Sw̹=bo2T.NN_oI, 0ZSrC)(,wo?*2¥+MOrrt:]}2qrvJʜ5γh$-kw~sЀBBj ˤMxc9.QQM|>FGGlmmm/T,ȓ¢zop)*.?cw0ήT˂@T*%NzON[jz{GTd[`sElʥRaegWM6BJ-qvr***N|@^|~޽lζ%f (EQ4MިNyNY\RVع{lzSuQm Z|)K2Y]q+tDA g0[cx<wY6 þ|YxX\&S(nIQqShv\.7:Jե+Wޟ5}hiF>\0o#q&޹FcA_'GGbӛ#/&*";? MQV'|D* n'܃-#؉NXvR*[wt_0ItuqYd}Kaiȁ}nS^4k|?_|@> fy4^=W]R@,t~ࠀ^=ߟѭwϞA,;{æ_`g/|oL~uk7<==>#V;ISg$gKCh@ NQf:`Ww_&+ r]g14U_`\ 4M=qjwKΚ1M `=vYͷW]UkxтCj W^mRشn E=1jۻmL;;s.ސ._dB>Rl$]zm7j7\FQԆM[̝ןK%eBklX[dS^&''ax{y{V;vԉgO0x?Opy8V04+^OG=Mil۾̘^aaS0rNQ:%CQ\ h]͈2$?lCTC5sN۱G0 L$ 9󼞽!j[ BzhbñriJey\ yRp8Dm> V@Bg_,9kPe䙤JV5nT&-8ufϮoLDBaCMl&q,X$2i$wfe)O@Pw SuQ \f(M̆tS&lwsĺ=D&7fHO@Tֈ3fq2ܤjwfe)ORYw LSuE#G5$syvm@ Z Zn:%$ @ 1?szRa+ @ VAi!^j=@ >4@ h8?@ @ Z*?0̆d,&@ Z% >jңa>0ο+9wh0w.M ~h xFҳSp[4~NPƣQ Ҩ0b*CiȠqu<ʽCw'B,#DE .lw,p ?TN`Xi 85_qqC1|Q Ҩ [ q*/W+9eꁵugZ-[gK'=>Y9Νp+mFGe=>i$= ]k70`- e?G\*&'F'N >thةSl5iP$QR&0R~<Q̏,D(nIRd0V@0@`ЂT7h)?+8~[WU <^dl^?`Sd ¸"NF#5(%SZ FCȀ1]` *%{b<1ƳŬ]u` 1 04?ì*IO'e,A9Y$tl^Z܆n=wm⪵6.٩HCW2)/Ȳ:b3/vοiָ#{Ƨj,p ]?j3LE..442F5@  aˣ$aG)I'{U"MTƷy6䤯VrK ϼp0j)G ^ty*f;6qF|pZQD~#0dtқ|Xhqk$=y6 һ1k~t  a[rU ,7wfg^< J7݀Qc#]-} MlVT㓜61҆!Ha ߰fm-QrWMZ `8qqAGc.lbFh]hI2jS0-IC#m@2O:%pEXDo!DcAR ¥Ba|#5K%ìx%!`୹1l)7lBJyy.` )KdN+rϣ4H,ϣEW!ׅKQ6Tq8*e4tZj}ᇓ'O>ym$|@QTqʹKҊYoV_O0aժUp/+Zv̘1oEj֬oVL}j2 sرQH iӦ 4hРe˖P(7o.o޼ [ E֭#G IDAT;u̒iذ!EQ7ٳg``x{n޼H$m8.))i۶mE[:tرc hذa׮]#,, oov-[Lզ㏍50777OO.]*qFFFJRBp?ϳzxxtҥaÆww-[FFF Q!M/.qܮ b}fu|etƍO,͛<fyĉAAABL6sLt~իWW*F#J^jv] uYpaddd .\جY3;>z9s 8֬Y֭[Ϟ=[Rhbʔ)E+(}׮]sOӦMΝ[NZj͙3e˖BJ3gѣWϙ3GP|ܹsU*󁁁o¦츸8a@zw׮]{֬Y۷/]3fh4k___xmqI&>>>*'Ԯ]?ҥVܣGݯՋri8j|qv_e2:4hݺu bΝۼysrrf͚u6jh 9rk׮ׯ_&Mܽ{7==I&MZnݙ3gΝ;#G|wQnݺ=iҤ{Ov۳CQ[  h4/i?qӅ{Fq93fLffO?$FWQ3fXjp_FFFb322j: O?eY6>>SNǏ/ENd///B!Hbbb4iRp\[σ:vx!xʕ+G^|z7!l8nWU&,ˈe_~eƌVoӧmT*5L8py* ڴi#Q* C:tUD"iqz~B!rU8A!B* B!\gB!B 3!B!WEB!B.dСO~ B BD!B< B!\gB!B 3!B!WB! B!BU|!B!g2lPŁBmڰ BBU%k,fN>B!rU8A!BoP(:y$x8`:+i;w55l (_S:@%*9V>qLLCF!TB"2EYQ`:e2_5uI$%*s6J׵koYLCF%|ZgJeii^۷J.Jy7 E+ I8EUϭIIؔ2e*Pnbcp5;-$kwmE潶l46iʉDʋ˻!!Q=ٳ3'sz])0T˓!$4L&l_77$,y-e (Z:$1hZ aF6/ p>*_nd}j XqsK9:u}OZo Iٕ9`~n(?~SXLsYLݏI2mHG}E}(qhv/B9x^n^,Jy*'{_kV'|4Uk;oƚdmM̪҇drTHv ^VӾ#O*OQgqJ8/a2h4&!Qf<&Z{XY]-NO#S:(yN0 2QDe=rE/`]ǣO>3c4tV&Ryy6Bd#,8++t;J<]Q\=OFMHEq8)Xظf-QfeZ L*JK3f y5;h/P6kf^W JB!Dq)oOC01tzihj٢$NKiV_N,޻k m A W0MnDszim""DڨBOv6h<)H e@Xœ$ŜTvA}kj(N]QJ'΋DpKL&kƸ{ 2/+u4M/A{)CRQ6Z(o2T//d"XF,Y S#G"1kFN*7XEd!xNMM{c<'S Mg_7k^CE)(}$!Vulxs6qEA+Ξ16hؙ 'y\oѪڲ%T:n'R99ݵ{xS~36}N"!-f HV*yK\zwx$&4iFy񼧽B 32X)h<qV+%rI}pƠdIsr@ i UbS\v;y<1 +NxxzJPr3׬Mdqq꣇,xqJJY7f88F0ǎm٤>}2V7cǎP\ HV"%IO}WFXX77Zqp_ڨ17'fV,&xL3m)IL^IB2겸ngF6ϴwB 3Y}|Is$wڢL&ChX֫{V;B lN7b5jXu:y"AÌg`>cm),^&cUn iV߲5Oa)ehZrynN]r#Ξ܎ɪ@|1xܦ:{&7YCX0"% WhӮCnK3f NNN,6שKX!\(-JX覼:u)WxoC'N&g SʋX7a#I#qFQ~C !7<ݢٳ+|FӴpX5d9gNt;qH5i.'1aj)(/W9o,uUJV; !V/ynqãYpz7ƹ?vf7mEN0GZ,J?BeBT8( 7k7VtZ͎m[7ikljgrB"ʠ33 $cbs@A#c`8XTL?kaI4/œdJHrI_2DӢ^}@8_&NJe@蛷fn&-f^$ҷi eV3+i4:jQcS'L˴;7% 3љL&ݼAk{b^06l>t/PE2/<_p)<_{ 2ҁ 7 Xm-\'=멜5ZM,k{P99T¨BApy`jGݹ].]sÚG>knٿNBUE SǞ݄B ^֓3Pϓ0l^f3Ogfi:yy{˛` J,(e<ȿDqhYQVu:N,i6`r8Hx,K:=3CW*N"I5l:B3SS8z;;zJ9Z9zT[DhMr<OvMqF'&Z==ˋUS:-/թ !]Dy܍(#CXʓU(iÃ'qz,G f{necgm鯏ghE*G;MALA*$Ւ Io JM'pR)<>8 YDDfӲjwj޻ yԼA@HZ-9'3 'j;wdR7YHgeRFa6V+0@sKݎ^L[V2N{}q,''8ncfOӌ>LƳg23'sry u^N◃Py B!\U ~qŁB$GaIiܯN7 .$k< `GDϔ,@r%7GuɺlhτL_AdV%!T sZ{TgϔIֱH|Y3 }d ڲQq V?yƋEDi]kv/TMc{KR{+_Ws^i2 !9mVBķ.o\8 5s2/W>%{B@r%7GA#j|3xpD4~[9=>! .]U8Y+i<À" SunCI~ED4#={A|&ERď*(;>KY AxmݤxPf6͎m)'+.], ø{hLyj˖G/K/ ogjݏGKGq^,z+ڶ6R}{m[5 ݾh)*p洧 wRc&_9$Կg!T*ps#oO 'm[B$&;u?0dD" IDATǯUA0TtO1e2DR]%e7$巻_ÇqwkWaJSzypba#wqO{!-5k +^&`Y >% C0 `UPza>>Lh^#٬SxݣSO*aJnOO9L%ar$dmU,&_ .p'OQ}zZ]{rhVeU ~)-9T~p.8OW͠?k7w.{ "۽]׻ FZ=z..$iC$~0M׮i ܞݽ'x0T'FZrذ1d o͖?,XۙӶT($䷻_,j:}t#TeT}ܙ|oȅ,/1RF(F|o(>5EV~g2߭dw)ihn|Ev >5:SN"?Yύ4(2#[4/]<Ev{r~ތx&>e /EgbqT~(.$J~j%jB.3dDd?kz\AƏf}}Y,z77>+ԟ;:mFx;u9႟ɺu)%$v<ž?ݯ$x_E='37bSMeek+8Ç>~p/ֳǓ\M]ٹ=!4?L0꛷,YyRb㩪`]Zx {VPdz\=O<=ATIed^^qOOSGn&,ec70 Ys@u,󿯭~Emݾ3)9˩zU#ƌ{m~0;ŏ;|;rɔ|1t:o?dĈ1]pӑcF7lytP1TO?1y/j`W=9UMoO?ݸժYN:h<I@5Ȉfp5 +,i)B'Ydnd_ZΝrEU1o~=Nx.:c $ɗʍT/.?,ĤQ i3cgI̸r5* ١h^Zrr ,ߞr50Wlݾ{Ylv1RZz-ڻ@BbyKWtl/D"Q]=1blhz9Бc_~+~]O֬[4 Idђ%7n y&Ӭsy˗ߟ63Ν{0'OUNcZhkdP%p1$I^tԴI~۟+JHL:m.G_5j8jttV-`RSS\8"Pua;Zmxh-U#GTT3Q`!\&[՗I@DxE S'Džߵm4]! ˲‹m۴8.2YV-|}'unz@WM ir%:H 9}ڻoXhI%M!T\3>z|7EDo޺5|Ƞ'C{yz@=vQ׫!MgJ 5?Ɛ3a\Ø:{e쨑Abķn]6s;)}s};[?N7owq :Yx(4Mߌf,?iJfV˲%J!s?Zzu-Ap?*XqsFf&ݷo^D@D[o@kߞ؛ƍ~cJkc'N:I\~XHo..I?OAŏ˖9{Vj>_/蟍/] >޺0 )O[gOgdTv=}MҩÙs m7o޵}ק׉Sgt:9vG.BH$e>xrNNnfKic"h┩kd2En.wv._~w&U&3 G xL.+ize !T,9ֺesBa۔|hcvm[jԡþtj4jת,MY%&MZtld2d*I?O Y+J07l޸ykiJR}'_0=#d4IҒWv4}}Б#IXGڧF4]u~ᡡۻŨM}WH?s~_B "[Qު|:>:~߷OXhS糳}}}#\AN/_DܽW(!aBSRS/_ڽk]{DFt޺Uߛw`y"[QaɎ-]R@*͖B9}\1p@Sg,VkQ/[>z,[2-=MV}'wr5 T d\(GUOٷ6qt#iW{u:2"\Tɔ7ge _C|ɊjDX,bqApM!g c}zxpsiM~%?{Ւʣp?! bQbh{󦿟X$#^pAtnIIб+{tg}c='] 2tgpJ5;{QbŒ?iy.G%gD4-z~pԟ .OhOՒʓm<,kܝuШAd %MG\M5pwpwսÇ㝿UfwBVX,w\UW2yꇶ5v0LP:VoﴴkP~Hrlه/׵iֳ.V۰iKD0!}wO0+cؐAKG6id4fkZkժYfM:{D"yͱ{RbXT*}grBK{O/ܻ'lMKOnҨX,ؾ=fd˯:wl_Q-kȠZ6ww!.]ٸn@o//D2q6m)]:B/'>sюퟜJ;߻uGH۫ǎ{HS?['0;[ۥSkUawX{ԩ釄Ĥy j#Vf|>MS25kT_ǯV+}{,]:B/X7n8t3/|֥yyx rAvEyyGHw'{`d>v ti9z=jY!L&*p~pԟL4",R|d8KP<{/CKg{#$I ={A8Ejķ2JJKO[KsSxU QM&k$I`D"ZUffo/2$.* /z}FoJfWu9l}\VLVtMrIV(n?87-,_Cr$/+-'sBnj{ƎThd*WՐ;ڤ)b ~wiV6쯁Yt^6.|YIi~<w!Pa0ݻvffƬ/!^..0IL?@IyxLWe"Bbc?=@TB!B%B! B/UTQos_YT v~ !\ƏU=ZeGQzF hk57jPT }toܸ?q̚ӧd^iG1?;u]9T!.~9X-&ecYvm_-j5rh@_ex_aQ&S^V-^9X$_ l!ڶii{KIӋʕ?;/߫ydhSΞСmYVΞРA|4v3+x)R]_Ӧ|)K$ջ!4H/yN<^ջ/fnťRP~ЌiSIX^keˣ(j-[4K<{Ĕ$1rؠ-"d2׮l﷭Z6>lA7m;u-%M/M4g۶ps4)ZF;vJ+oZ6yVdiaճg2+R]3&&lO|۶X k]C\l,v=l=xM~q~V.>=~Il0>o>samO}rvc,}ðKi#[7n_hذ~)K$,Fz&)YxY75X,tɂeℱ[Ϭ(R]K^j\ۡ/UqOfٵIoCtһwJWE\ܝ >lub=u\%T1*Rz>vs6`st36޼sW=DtF4S2ZlѡC[OƖXh<:u|~2EӤq3.Z 0OiٯB~(oA>>xyi֪&T={YAmٺ+""LH߯?ݾs_ߞK/-[vMxNZ>=vsG4 J5'C_:4OJT>4mҰuHۢygxx;u|Ap'mQh,w97((p>m۶Em۴,D*Z5!<(z=v8uEs`t>3\"t^[~2kޕ+1B޹{ߢ>xݢe'O ic>>/~;s•+ .NTU8s*=([_*J9ƍf!pȉ[Ǖ46oߡ{&%&{h?qo IDATɊlujjƲԴy>77Wh~y'%*bbGE]&I2&&'Sko,aBbN}m;8Zz̽Kw-ɔz>Cp+H$Mub@߲Cc ?ǃ /"CfaaxOiSvJ_Bn޺ӯo>Q?֭5""Bu[5Tem}T*sss G]Νy{{6jLdIKϕRRzR'V' ޟ!zYP*B{?)Qдi㈈p6mdJФq[viӹc;!M iڀWuXzw8yқ,vlfŲ +R*d2x>SQϟ56RCIEqwr6 N㳳 OgSǭ{u4n۪5?./|"1)eY$I֨p= `>#Ƕh?8:nݺ gG%$$EDfff֬VU ]̬,JIQԽm3ˏg}:OqWtƣ 履}^^]E"8K7kz0~oQ|[y'/_$1޽7oO8P9=j'Eժ]3Sf[G_snuѳԩлYTǥɤ _XL۽~`!8Kڮݪ-[ MM$IN~{ܶ{>׮ߘ0~>P/pQbm) U( {Bw6GvzЪEDբzzzܿ_i@ |k}m^˲lZ3_&IDYY٥dMP/{ZVpnTV?([-u7שSQz+P9uԚ=e>xԾ]kY|| A~{wpmS*s*R]Ly e[}Fu\ys+V+X\vbz{w߁Qmr;)@zWVFP.NjX+Rm(v}QAJh -r\ݑ OrJ6Bn~aQI/p8/I/G*}c"J<.4=k洗egOZp=xZoq(gs5ֲȶ<]I#Yk^yw8X{s noZawy:kaRtMɌ.}}{ hK/vk^L4DGFϕq l>R| >#ُN>"*UioM.e計АKz2)ܾl…` <ϯYs/qr<M\O-yiJ\\LhhR 4IR.\ԡG}ahFV?:w+?*/Yrx ~"/SDŽ;vIOۋ4B.37~Yoss'_z)ˬ~ B^aT\i1 @( ǥ^lV4j,%ݺSH j~$"T ${6 W^VRB0+jO.6lEviBg͜p7p=xZoQ%%&Z=kZ񲲲Ȉv>Ofw:Op 4V-ᄏfa]tEd2orRRwF9\p}!oLձc¯oKv65E%9_o,}W+ɛ@A6-I |H}&`R(T)kޣ:-seA3k,5u/VT,BohO\CtO-7/?88( @oԤ;ҽ[```^~;'M/xS2>ᲲrVcud&!DזorҎg;/ݥ ^sBP]?#wF0ѣG?7~ru;^(~ݏ}p;3˿❼YK.+4UPk/?ý.DE2²T\(c;IBC(g\gmCUft:KC$65 $$$i/=7B_H%%%tA*eU})qEXd &Re{4mxM^ ̙=fͽxrmu[-/p8>[G7-^WRRǝv]ɍJ{۽@"tܷ/ {{~LJT΅z!}֌:yǥ^oUřN5Sᆴ}}aop^oe[n7ф EqR 4ɓR(=ĴӴ=|Nn~A;+cŅ=RIs-9f9{YogϏy}O$}GԯoА>$t];q/1QFcL&4[mZ/lSN6d2 7 kLnNjƊA-ͽxt}C;E04=j=bx2w|QQ#sU qrk#X㹏>˰lpPGf6mgqco,zO=}|!5̾ȈC+~<)qݚY0O~A/2 =buC$%5//{gPgeE1_АG+##ý<*/߸z-We2Y>=?{㦯lv;pH=]:wL?~dwKJtZͿF:L[m~%/h>.pd{G#otj_ST ǩӎ:CBs/$<&)14jjgnikъUΙ9%eX۴4ݖfLVx7xs=)=// /J{sDDoI/i<8 ӹo.8~R_i{^O| ['xyTNs'^g?p;ԃG8er<\zLYԃޞs2tn] zLXeץLJ/h>.#F^k{ʬ5ݼy')G6P]f ,e^lv'i5n[X8o.b!@?UQ&" A7ZcEAh8׸x+*L1[JDmp [7~k(rޟ>R'~Gg%$I_uvC[ {m*ݻ\Ѯi_pe !fȺeB!g\UZ0tpYZS5\o ]o(kB!'p$q++E\Lu#\'^ZjꘘgL}qQ/ !ode"G O[ǥ^+UJv -ue?վ J?<|Z+ɫ -0_J6Wc{z 72zYeFͻm? 9]vwiz DEGNZ~Թ6pFՒ5ߞ|q}+*L̡oVQF6~euOB2V2lؐs<߇hݧ_428.59z,!]bBSO={dllLE \*M1GFF!L7`Ȑv8s.8.^˯;H3ѭ]P}БtA!>8}Ì2e~|5Z;B!j?B!j?sI鬃-\p!w:&uN~}j ??SU1Q(qwP7q-voEo2=teg?ٰ^pޓwh}ڲS>$}# ^Kh,xh.[V[|Bͷɟm2)x䳍Kd}Wq|koY5i_0HWH7}_z);U3O{v^.)+6?C:u;uC N{oSHx[6O((ad2VWWWI;֯O/\Dvtsw11+*?!!!I:VO^!RpjjĎNι@jlLtr7 ҏ н[r{VQaڻHٵ(RH?veNBCERi{57sUNS(?M_<4sT]6|YG5<<< ix?c9*RP7AxDC8pJqF3%îV靓s$<,,$8Sݺw~3Y` HOec#VӧW/Ry)ؿo3Y߯ ǎozkEaa)ݻQJ?~,s=Sz ?@.p-ۻWHptOtrI:]~}Z\񴽚V2W̺U4M/)-m* t[o^T*;Qw~Qp:E)VW<  6u쒒g~M׵HPԛ ((pMX5~{хܢ z{ h4%z?[M'o t/Eb Ԋ,Kʼn2c,$4,xu:o^oJSyE4,*.o9[j[t{o.?Ͽ >2{óŗKKkqaa<ϏZe@(UJz IDATL&N 29;ĩSa2_I@8%ߵ챱LY8VURv܅ y ,,!!9[ΖT*=ѧI :~EQEE+V`ͺIw;QZ,ð&*2o_J:'&DGTp8N6mŪu_/Zr$-]:dku:Jꖄ;qpT*]{ťee=7yaM}{fϛ8%v}'{8BLC$u^1 ȬŰ7Gx872Y-vK=xIןg?Lvv΅ ~-f+0 2LP(yAOL.r2WUԸ%%%!!]dCjG-K hiE>oiMJ=tH=_%$f˯6LXJ .ļ`m箔&NRiQ`z\o<_{knk?wy_|iYEV^zUJm,?}}(KH3&:jNn~M#tmuy{OW3h4p/Xm R56Oj.@BHÞ2 3kJ((:l8j~' /^c롙DA4nzNgl4]m/8aV j FkLr\ZC(@` Oz2.sI(@T/ǭcWDj|¤jgI#*R4_MTV.Z)cYV[^Nm/h4 C7,R(b\av -ueӸ?qe97<}TzHT^Q/>Y%t,V{El%^z*Y9kw cxI/ѝɢ}.} tޅg33x108`7[9Z+BmZg;n)91,H'&YQ{ipR]L;%$7,cؤ]PІ\ qN,5O|4OiK}66vK$Di""EIպ3ÆJS9RYP^<ȑ:C=8BiD)A"V2744áZ?Zj0(TKUF~Z!GDՋ֨5ƪ86a暣(+sڇ@[֣Y,k @+mVxkVk-0PYRX,Z + Qe5ʡY}fF;ψ4m h<;(KJZH8BKz\ x*3eyLH^HhcV+G %zs% 9\S[ ]p5ǩՙ#vN[c=(ȡ{J**e54W*ycGyE%Rz(k_!%&(`vβMe:FSa;хyfڬVd[Xǜ*&W4}"sⅳ#tm3ע⁃BIܴI#$fg.M)lU|>3#PFK2\0L_}%R9Ա!^%>ީ˯A,w,KȒK33<YR)]ݫg_(.Vi~p%1b GTKtǏTru5= ^8|DEwo> ]D+9(2Y3jsf_6Mcڗ{'%B(4|bK/BNHܰiݕqG/վk>^(m!nIu~GSoaOmz/]6{iH68B!B!Vp1 B!_5f\B!^A!BU؟A!BUx2j1ct ;sƿ(n69&sYrno̞=pۥBHc֏K#ק{9}ۥqqq&==ʃj}.Mhiv?؟4=iҤs}yyy>qDe#YRR2zFw 2aRi67lpAd4M2wܹ /('NX~cy ,N2eĈ<ϧ~I|Z>ywBN3L&LXlY9鍷zkhhhvB!W҄r>_΢ Ǜ]ƏSO=ĉ]rO>I7gϞ=tPqqq#?w'N|饗f͚nݺ{L: rڴiӧOcbbO26mZXX?`{z_קO Cv]},Y^Ч !va]{=n~V;+>s-޽mۜN'ڵ믿)))q\vvvIIZڵkZZBy=rssʼ|VٳF9|paa!BMj*Ǘ,Y#W))) "--{^҇fff&%%EGGgdd={Sy$=z())̔ Z$B~~w}7eʔ˖'$$K.SN͟7xV@@@@llѣG{l6[o;HMQϞ=YٳgnnnFF}{- ))))))6l {5 w'O"sPZZJv]Bm/|/S>j3ע'O7.88Xzѣ_~(ѣO>AAA`gϞ(Q* ,(o{N\\K/h.]ګW/0 ѣdYYYR|}&%%-_w)}^NhѢ+<<|… zZmΝh:tcǼ{/]hрV]hԩSEo _xVE1..Rc)))#qС… ]4M?|Ͷm --zǤOLLܲeg}}yy4+ƍٳF7d&00ܹsyyIc333'Mo>6mZYYٻ+Fǎ[Rqqq>uaaau---׿x1bi}z^6 Z-ӓ붋 Rs >~ϯZjnWUa}im_/hvcx}Zʕ+}Y/z뭮E jKm۶~iZ.$$RoPPb;ൢh4z*NsfgAڪgϮ]~;>}̜93;;ƕRT(n2z̛oR 8k0 x^y{- //鍊nZjUqq]wUkh_y.vK{=n~A+̵H&/gΜ z4M=SN5G>pqq1EQ۶mweII {w$??_ P˲lppkBC7Sy Zc4~ju:*Sy œO> /HFEEIzkjj*++;wv+Vdgg/]tƌk֬s%&&Jd]tO=})i<Ӌ=z:upe\VvAiؾ}mivc8#ɖ.]ڳgO^oasIKʂ:w̲A\:|\.8q=Ͷw_l4 lڴiƌaaalٲGN>]r„ ΝrSQQqԩiӦQh瞆ӦMX,NNdҹ\>uTע-[L^j?xvv6^^I^^^FF$==Jj}޾㶯W]-΢v \s7|3k,F|ee嫯#2;;{߾}K, l߾oLKKse3tUPO85kf͚bŊJJ~z)oR^y啪*6m$}yիWKcyZ=Sy>yYplܸcǎ1:((hÆ q\FFҥKrnS\\_/]4''G޽K.R[nU(ʔ[nW~{޼yyyyǏg̘rJ~mtӓ<ל3Ґ / 6m4i,u>}VZ~ivAQؾxo_8km_ۯjg y7on"hMkj :uI?˲u\t(<*{041 ^HJ2((m>EםlrnD.t[v{y)gC׉.On+Oؾ\}xj_q'~lgQQ}yҭ[AtM~kEt텮6ؾ Ox%PӸo.㸡CrtRA !VJ!g!/!}ф!B 3!B 3!B 3!B 3!B 3!B 3!BH!B6d^`!Ps!Psi"B!< B!* B!* B!* B!* B!* B!*Ipw!P[__y&LB'cR B!* B!*ƛ! -"D" J>bdTWǖ˴|џ? BGR"2-D.2uV`⸒D傇,ܻ0ӡWib,0BmgU!x`u-B؊UgV7 D*U1rVJZenL Z&:[)F0QM":И\CX$UA!I^MO!ΘqD[sȑ [kDJ HQ Ң+œՔwPUJ'1c+fݐM_\eTn &`*y'9*\&%N+9zԼgbn0$՘s WdTa;(4g?ZQCzBqKA|E(#*!+MU@Ʉ9E*&{CPnO; )6us eN3LM,걥 Y:$ {):L(bļf-?Bm gP+#}{9 seɎ)3#7r5HWKYS;ez^fpJdh5yCjQF-tArԻZ$$ B8$<]Mn 嬬mf7Q@ylX&V#TPR"NRqDmɗըc2QIVbVʨs<՗Uuab*T\l"jD\( iN2̩pt{*ΌHWJE нFW2ab2IDs\m!8 U~Te9>y+҃jBRF'N/N{9|Fa/gFi)j;T^Η h Z$2`XPtjХFo*eɑZAH %0P' IDATacux+]q\eɑɍNcO)C(EӔk(CMlT rS,(l"3)8J F.#ƌ-'r>d9gdFd ^ŨxQ$*1iiF1%U41ڔ>2nB)-x! >:UUG;.&X'%قKS}B2B -.7r?Uy'Q:ˏ ~uQ9?O([ P:O;*Y~LU6RC䲊@'52-;hSwY XO~K #H;)ޣ.wV;* SJ Iֳ_vBD`#Ei;SR's[_KyQG;>P\_[&WF-?f42AISˏa2 -#@P_K(& %oe!NUw2A8JNie \m-dy(]MFt ͼdE{ڽ(((pDt"Q o'%4@j dRj$g\Yp*9c,dE)٣yh@JT!fKzpޔl)B* 8 8N3#sc+ttjhшr#ODNP9i(EI9C!pD3LA|J7xW\ g! wV @͕pqd52 ^% 2Рpp6JWCykMnLuа&}'kKO;؟AVJA:AUyJidoe ]jƗQr"O((Jn#8@T9*ɥ|n)ChQftg".= @/nvTѢplrS'@mE X=穾3"n0tSYM>_B]Uo $])wPN UyRu@AX5("%@H@D gE{Yhm]҄"8Qy'L < !P{AKV`.nbL`u"BQ+2r;3 v#WX <)=.;b|HSS"O\o-wYzD6WΖ A=k?TVdo k|svc`}n5}_Ȳ> =7[" 01YfB0)ة;, -V`[@+z|I]e <<| `Be^njTUk]G[ܤR?Wq˲7US\DT;㧔ϵT_c2քHC>aJk !Iy{Ozy =>;qF䈽‡[joK1.ۓM'#j(ƇY(΂JBrC_޴siåGKQ+۶vU-\ͦ>,ͫ4#a4It(Yȋ+6!e% k YQ%en-HӾ45>x5_z`t+iEq?eorq FAg 0+D`3&Adյÿ"ÿ"{v&+nFB,vx„_ ׷!r> !p{=nE$}IZQcݗ Ns71BG?=rbΡ$=WWH5|~-OQi hLUivК/=v0nZ˕k8ii'$dǀskBp׺f`oۢ SSy-+btOn j^%_Ee &̭wI~ֹyAy+:HBG^ #q&0qM|e46u zxҗlFYFoy_ KBS4^;h͗;j7$ڗ\_Fc%PgR"ȍ_`JdEa%XiSL T ,xm&I /ȉŚvp:xu%;!i 1I/q6DiXV4 xu \c28P'0V24V;h͗}+r2|qSJ渕BJqq&iݬ`;,\ƥFo}:dL+wftX+_zъZ8 X~}&g:iXjy}b3NIMrޢGV (!I`>BY9J'Wګy3 `PZKfRdT PЪLJj>*YL.ivЕ/=hEO| DIi>TDcgcb eP^Z&m}Ie6o51$<h;I^woKZH:I;FH" zӺm=`S51q4V;/zʕKK4^ph'3*14 SO<#(0+J+D4sOlUum)yx ȗoi [v'mXF8=0iۢ,Ym ͹q\>K>'r%JJL-G[3:Пt z.'_^Ddĸn)yk ZzVge ,xh9<;6$"\'r|{lm@>ZeaW>UVx\UU 3[ol~vG6"!#^ *?xAv5>gVe,U䩍n@ցs("6eeE )mV,XnFGjRSjwUqaF @WX؂wW20jˎpo"F@QM Ểw ^`tl>xw%E(0]4V;ʗ$~r@ JQB6eQhQ.=Kg;˨fC[1N#sjNZ(|dEhT4J$xa%AqT"ƠKj]2Tį\D0]Y+w6bO0e&c1a任J5OkwB>؎)$N5t~n Nmht6xt SbUVÄq~/}Uv3wR>-G4`r@ EHҨmXyG>+h²IߒK{Usfq)+|!EtAA)%m4Ye`V.}؊PűvL}^"~N3[\O΃JgK3FiBi͗;!v˻0ENuȑaÆ鏺:=UR%s0rI;Iob(P=hh|v/=W1nj2t`id~0\@`݂ IR-}g7Fb$w$0  ;og*c@|=|8KϘ2XVu!g Vw t'k{! DXg*s@ >HcZ:ݣ5;jQA$ U+V/.]FE|鱃qvC Am4@ Pi3׷ ojEbJF}@ ڂg@ DCg@ DC1gzniiC  sghaDc3ůk?57;'4>T*]{F;hg]Jk7n IRv:D?0tcO{rе7F;|ԗ^@ WfL^t.NOA'Ow9=ܛTڹ@PFvpj}_GcUڹ?{n]k#G >@@ ,*H]z|/LV%Jݷgpڍ= \lŪ#_ Osr|fϘ?{7.-i\Xr9{7v-B#a۲̾DUc֣[WK i_s̸I$@}9WPA,k>S8;%%%oؼaÚs@hpF_۰炢'O㌐#&3?ٙ칌|ںddf/\Lo>WTO}ߗ) Hn߽/5Ѣhrҕs/ef^Owd2 ^=.?y̕7 .wr4jBqqèw,觃,,,[&p,tں4qs{7Jg' :"4xUmY7Gtw}ztsuV -[\~ST*//[]" 8(0c'GG_A=`cc:"]hHH?ڶsϬy?p0+;{3Y,ֺMHK_F$D̜0)9իYܼ}~2cjBu訦BU](*ݻ8GE?wa^~ENڳ2g̝/*7Ril| illm߳<%Py@Ԝ];TIIIXhFضuĔ6yg#]]\VE.ϝ93?ǂYR <,lӺU~>>^^׮ء~2cj=|PMwEVS_ZEwhߞ ڶvt鲃V{Vuk1)Ily+cxgMڮm:ʘDWNz1Ob eė/ 6>0g IDAT~ib=O0vL2۵w{vܸu{ߟ4\vkjA=)|}a-޻2f~|E |}98)8ۋJE**w#͜gV[f3@ 72:wރGeeo_U,4?֝{"ݸѻgwJ`0,,8ҢǔJ*U :`̽;2fMT*T]]Z 1Ocg[8uԙC 'R_JR+K8Lfs@ jwX_PpڍvmZYZY&vVP>u\|>t¥+\TKzxD l+՟ Jz)ʝ[7t&aKU뱃V{@XhȦ~ݱ1uf@Z^~#@ϦMNi ozmAa\f wza\v-;'g9T*^q>TP77:jBC/^ҿoߨ93S{ne%a$`c93?ь ";vptpLb1c4lSڵd0 ƙsO` Z$) !нkWg_7ѭKgQA*JܼO{~وPvm[zZ˔2$12&3+kMΜ߼nwf*1A=`@~Ϟt'wP*cbcbJe>lۑ_s~g<0L %Iڍ[?.]o>{ᢦ?k!透n۹GYYYT*a-Roܺ]W0/ ɬyB?ToA=͛3ٛn;ksX?| L&Ǜb )LzAkku^=mVr|oEIJD% +/~ݱ bʌc9R|P{ 7X;XL gŲ^Y娖JJDoق6$%%P.ؽw{NpP`yyy’aIɹ=<=G,kWc^HIL&+fNgv&mϱɯ^QAL&Kd$k\Q..^c.Uji i6ښ?|O(#Ǽ=X,֤c=n0i3W_ffwy%jϺ.pߧ3ZU4RF>rʌ^BaIcw&96m%tM Xf}OdgcX۷ԡCmd\XqvvYqJmu]";gd;w^=#]\fZ&]qOԞu]q܅OySH㯃6oHy*X_v#$אP(8{7JhTAX+HTZZEUnM.RvPLWt@  JWn*j%’V@ >,@&+OzCrMk4շH: @ ԟA @  ԟA >t33+0?<+gR3=CvC b/\][ a>ޞB6uoP4ռf3$cwA)/ Zivb$I4IIkm% BEtƴZDFZ>rărG&_|6թjyRY?lYG IbЁڷPyy$n}KV ЧUDZd-?ܹR37< q(IHHs]ϝ_PTǯKu\>}>88W(-ȢBY&ܭp:g]xW~~>NpӔ*_Pyi"x1s}'8yQZd-ڶ q㎩R37m۴JHH,q*El3]|޶Z/sݿGqqM;dfkHB\ Ob-ǐw_J-lH #=kڱᣘ3g/,[Ioߴիܽ3gLo\P)?XRo޲񓟍 /=Y53Bn-Z\xm'}i3T2ԙn(_޼ie2qB]gV^fYﵫq"A>gTkzw9`OUh)Ff$))y¤٭[{鲟M~g{-X }VCܴy{}+߯7t:mC8p:U) _H2BnOsҜD=1uL&g\TܵB'Ku\erĮ{ #ƒ,plȐDF\g v~d?tYY7n)*.>׮NJJ6U3%kdtzdv_OH]%&&TSXX*5nP/sԨ\@i芣O:7b`'G{'G_S'7?9{u3ZyX⬆ؚ_D4uˉA@\{ 11ᣘ¢ E׮NH|&/҇bfNuSO8s}By&r,!7xlTe4e)-뢵y&.DgB^rZciiѦMxlml4А\FеKG??_}#yQ JRݼu-;);k7ql<=& ٳ?,KBO>sܼܼɩ8?~oߞ9i>}IT/k--C슳,,,Bj~-kk~hHs:sfFwVavv"v&/|^Va-CNu}|<סC[ѡ}&%ebk NV]!$ ?skMKXY>U{A/vytLl˔o-$g.kܾcl" fK]vj{=yIIO޽?4Mc+Dbee$Ax2^UPPy"AʍЧEh W^egtؖ*7m -rF"P7w91(}{x\¢7]g/֮ߒ:c_^E KEN{\vFѮ߼;e=TR<ϐF+-e9V޲xط0* 0SHOO[%-ZS_ﹳz4m`Y_ڀHHLoMOS鲃VkתE vZk۪SWj$ `8֬_Xֵm`?ʍzwpquu>%Ug͜"vӧᅜ|7ʊʾˉA@pp0 Blt 77gG&sL֥%!!A~ܳg\W.S&}VD߶em@SʍTRlh.'5O)+4~*ٞF{*˨jx]n* $T!)M昰^X346?Eq%t݂ BDS+peM\_M{hJ|&5h3FZ޵o/W?-_wnRӨU( ![! ΢<{t:nei9}ڄm+.ʤre8egez_z_Qs ͚xG,~6w9yOP7jHȪץF0,6SoaznkLZfgTRrܡ9oE=_o1$I`mb KCk~dU!+.'' G/Z8ȑ%:A\] BEpP`lܳ"WF'M&Z9 AF".]=un̏$IHlkgghBB{5V9rNhPWMHx*Ukk0w9yw\d0MG iUPb6h`-[:B\j]Z/F.Y߸|+VkJ)l Z^\p66e啧@%"WW h4vrsvc媍Æ ,y얙moo'⟇7n~l3=\TTlŵ x" 0>[TXhhbczF=<8^i;|ҫW7jIIӧX,Rn:wg^$N>m܈a,4>օܬQ؈\;1:63fH(L+GiiL&ӌ:;;KŚT* ku $x{{rVz&u ñ&n/] j,$$(:& I+۶gC4ԣ ۷'$:1tCFaXKhrr'~5b?irU*!͛7uʸ'hznڽFYI,soC./Z-TgٚQۜ<.?3 ;[ڤ@xTXg1.իgg/bcÂq Kͮ}2eWY98(iJK_PP I_/ZTkN5޾Ϳr=BLs/F5 !={X*8ym{jꥺy'1v۟ɸh=! Dʋ@-X@cVȐyYLF5NN?L ;~Ź'R~ʧ(_%WTn뱛իW>Q(qn^WGR?5cNO,.*ׯqr~^q ŕ+7jsLwbr)*&LQ*D}bi4<=#6NIrHLy'c,82ⶑ ?@ў,͂S_8^](꼼[Xp|= :i]O>`N:޸E 1{|ޜSe7/ϦQQO3\]]](S粳s&NET] F69,i]tԎ`T_ǏYjya8W.)^$YGa7mwuusTˉӽue?f2ᡭZԳ9{@B̓㾈wnO3߸g/%"B^PPZ}x.UMKeO_x|Y5G^ZP^zK$cB-̨MVkvP/&Xˢ(+#ghn]S^.MNI=s&{'Or5*h믻۴֠Q*'NjB-*uDD ̾윬:C=1>..;o~pl܄JhgTko߼u`T_;w[pֺ?^M1 #ݳfNsTˉ755ᣘ/P(QO=zǹNdvf!ܞ/7XYLmXӊu0a!KVs륺yLFn|i*ަG6lM[UI1$ySKOL&WT\+-V%ee*O&T 殊޵I@_'§ LNĻ+ ]\(Y[jrHh AgT-'z_fvBzz֞Pq%I,1}'%xɓIjuP/Klv YG2tj1E`eլAHLftXeߪ%Nk beiIYsWE 0oq§ 6+ ]\ Na89LR,uM[N<\+KLnƎZCٗm\{ IDATb]))W.^ޞwV^2sͱdjAlC>7,9&lM'T0WccBsX-B5LJɬs=Vk?(7xy5hFt7 _jqQ0:\7=r5L&I@1ƧoLhBT躿B)Ti*5irj'&йsW^6U*Uff/2kZK~  ~|=gϞr΃3 &~XYU"եD INIݰa[mZjP(3J˚ϿpjVv#_iin@ձI}@ ?MGv,A D !e2E&<[C_;@ @ .?@ @ *?󆤗)1y 'n?_{  ?#/x`!p wiw f?NdIHV^sVΛuFZxsS9hgK/jl֫jk7nt8[ܼ}秕5?CB׮~ƍ{$11=u1T@ jR/S˱{\MMh[_!uκe3|>p_]0!8;6Xb哧qfvڹbЬZՆȎ[@366nޢ];w8n w5MCi3\ƛl=,.}q y_x$y{†t '7/>1bPXrCNU?,39TT,V|&˗Xu}fUJ]DzUqcQ}ECmUe̴m6Iy"(\/[a. 0ɹJ4Gn8tQXhCGJ6:oldt\^#߻ X|YU%|~J$*48hW/Z mXsAQ!W4T@|?cGX{ 8?"$'p '7ǙLBәL%H$Ob[i|FE?vwo`o_PؼY@^n Ba[*b$:&F* s||*GQM77fLNnnl3$w.|뱛P(\E=/( ё >|%KZjG27RfWmyc)u?1T@ j$%Ig4Ygi)3.DgBR]r B /*JPRr++!Ac4V#[g8;99={ԼWӦqϞLN y '$˵  p8яc (a눈 :ugό[=g +_z&۶nb25A- 8(PT%Lˣ<^V\G4_㕊Ke2Y#ؽRχ'% :'$[k^^8$tMZirt˨?ڶsϬy?p0+;{3@(,y(zߟN:]9Ĥqݹ 6I?xD^.{jw?\|d 113.LJN~*uּ7oߡKn_N%oڭwRGQwq<*+I/SΘQ*O5 ;_T,V7o̹\JHmg=yK:h-L +oYI<HGKn*rܙ3S?-8?.Nm:bq<곑..VuVcl'+'gڄݱmVGbR_U?Ыv-aaܸ֭vud\WtۛoΛ6y"%oޡ}{ "[kѡb<ܛܭ׷iUag]\:ٱ8;9QBCCBѱEh@dK8 D:P^,4(JXęٸT$]nakM F޽O;"?uttBG 1)Ily+_iӄ_vv]z̍T|:.W( ڴj_PtzBܺiݸIӊjAr{;:ј[?/pU\Q- ZUfpQbs9L8h+D/ER}b@x/_S߾kOH b ]2(a-Bioof#x dU\h윜윜џ0 2!M*Jdm2r$H$^kLP(ttt4T^yA  +}ιϽo#ư (ȿ-Mac0b. ˋyW;"<ڍ[LJUD%2b}jB`0L{]Юm՘ݻ`itˠ<5vbAA93uD&fWqQ`ggV\] IVj~LHFff^~ń\ |9y̓q yeyJ|V?WBzYLCU.h4&d2|,ư!g'g&q(%4V Tx XzEtRZUgLWZ*H$y7eddY[mʳ0ptpH2uaQ{=;whK8v9|E  AYA3ͥhNպ^64bPYp8jZ+ʤ\yP*P9abSXV4rIIlUesy< M>? sByMp؝;uڲm'UQ{57Z&b_#Z6$%%P.ؽwT|C8EZHnv*^Y8˙|5$P=rr#51g=&3++*icF'zaR5k5ɤlIL>a?EH05$.--//tK LFHJe7nݚ>e"5dgkKhGϙ54 Qzw1ooO{;;5iGS #&rۀP*)xv&}R?hOfqNM>u\xmHRf}ݩC{X`kprt\hfؾ0sBMNN?9elo/O{αF:yeȎV^'-n/~>;L5bul߶S4mi7mh[ltml+W_8~BJ%SzF樱 zz899"zӡE h,e͊Iq%A Tc,MA]_58^=LpmZ˭;w`5̛sПJ27/o՚a¦7S㯃6oHy*X_v#$H'vE Y,.i/I7oٲq\.};wjM[̜v?\K_w]kˁC,Z (ܛ߻ST9#G ?ʋe4VUJ$ɰ @a4M'R#!I$IC0*}>^Nk^.b{{D$AZ[Yp8UI+JXlccwLD"Q/BmiYqCD#Ʀz}5y ^%VIqBQYZV%BbJ5S"8SQU*ծk$k%L<.NݚYw 0+)i *Jd2V\nqqFb2VVVt:]96j)G Qyr.tMPԪ1ObEbaɹ Q9HkZx\nmU',8*~Ls &Q3/SQyUe8>d #2!^JnX>o#ӻq*iTKX\[ ==r1#*-5(HTV̀TTvUTLCćF]{LWt@  JWn*j%’V@ gn5 2v+ԏh{E)۷׸"dٙ@ @ g@ DCg>\H4`=n`QO?cq (DNUvVE>p$ '-/4 4eڕC﹨,/H^xͽGħ$ "G#! 7*SFhTxkRkUűo A>t %g3@ 44_z(q5-b_1vZD񴍞SPR5#A݀2 tL$oCX_0wrg30i0p+F̲d}4GE*{f[JS)KX^dևoSdKNWE16 ΓDOM+G  Dl)`aR F\8!("9W$f)B;>2}3e{˦FHB PBi"Olg׳T XPDz ilO1<ߏ;xRE˴-J]IGaWv/.9MBkyFk5TU4ap-"xNB5R̜Exרo?3B#%ZyNj,O2[e$Ͳ&Q_vBr#X)Y΋m Z1 c+3K,`O*Y$Ξ, k_v4؟?TF:f9%ah]3U9տ *%x`tHb}>C~D~ ҃]LXTC0~d!9 9쉧Dy*%X>KGhFj2$X&9uRXJPU:B KAé@bd9m+]< ׈r&Y -\-H *SfQWL8eġ6=,tӮYq;R$D4HJƨb[mRN:10uJEE3n==tQb+U:B }nz#.z2{ KWX7W_}3"B!F3!+@!\3!B 3!B $CpX8}={"Iw/Hz HVOU{˥QQQJ277wB Eþ}Q^k L gEp O?q,:uj^^NAQQQSSӂ bbby~6me2`/O:շ5M[o=3,<ܜl>gʔ).WD7tӜ9sX=y䧟~j4=ojܹK.U*~ܽ{Vz}tt_xI&eee[q]#aҕ}~Qk Ǜ g˖-x衇뮃X±H"<nJ?)++hll~WX~}'nNjժ[nerijjժ=={__|qժU6mz}||)60+ +{k׮أ !tųvi6X/pc;x𠐞$X)--mjjR( 999--jRRRRJefff}}=jժ 69sfڵ IDATB~B$JsrrSBU\oMM̙3 )))))/ pjǏ_[[/xyy@sssII#sK/tm_FyϯG\3Y~~^'Gs]w]PPГO>.2e3s yŠ5k֨T*磢Ξ=+()) k|'f)00P;vli=Kwꐐ7nJrnx{lܹBFIIIYt… xB҇틐e2=mל dyhH3W_}P(^{cǎ}uuuB>jԨw}בa={̛7̙30nܸίȑ#P\\|7fff\{~رcyw0dcbb6o_?.n֗^zIxkMIIvk4S^^;xGyD\TTt 7;v VZ[o ˫ڹPQQQ>,  lnnvlssF~ yꩧX3gCBB:wzruuu B"7ι]5j|ٻv 6x^^^A<<ǃa}iힶk0H/pƲ裏_|qŎERl6wʿcǎ'T*:upqGTFEE)Yf͚5+ ""B"43ɔ HYe5c6geee@@H$ٳg[nݺu=wkۛ.ܤ$J&߳gss<K/ڗvia uuuΝ;׬Ys1&D"z㪩̙3fڹsgrrG}a:#6mvODgFy`k[[篭4F]~kjjZرcϜ9zp6i=K:99fl6?p+Jeu:/+رck׮Z'O<~p8nΝqlNHHJ~zow}W?\ȿk.\K/$_ w~>3kwww{<ȇ~hپѣG b__/),,\npe<6mZn]eeF9tИ1coٲE* )Hl)7xG9ş~za7x~"IrA.c%?U 5@;x.1$PUȄbJ,,UX 8K H麑3-1) Pl& А44I~X]CNKO {vc+Mٍi)AU E Mfr8I% rrDXU^Uz9gByV/i:MQLHC5G.Z eƬڎZozcÑB@2Z< lF|GsVB9lj@LB!roX^ebLQvk{2@LJk:jluy$\-63VKEBhHx.1Bgi$/ ,Bgh7VjmH UFƤMv1jLZ&RAaeUHTkdLe S$xF_K~k45v؍JXhR$Ʌ@ $IMbR Z -5D' bnPKT:L+g6Զ[bE:LNXojXocm! _w]^ kWZt2,L-^ğ4Ȕbs<ꅔQ!@05x2IR֎puXOSbR4> Hg)V)#x6ÌЩ4)[aXGj]W+v،jem`f--V9- T)D @#,[jfʶ{x@T AMұ^2Ktx81-#&^Q;w(4%YoᚣƺAE$mX)9QwB+պ,T;'l6Am@E(Mt<1W:lbRLFR"CH1"Ee{e~k4gh⁧byVBK쌭 5bՁCv;N t6KRRR %%sh7o 7hQ 缥:c}I[iWdr/Ga 2Xpv c麨Դts67.ڬ43ZsLbc9;YwPL T[?+Y3L6Ih1^Q&HQgY]HlT(ѕB¬-+k]j*֕'< 6kہC^Qq^\7Bne-z@a~\oGKo3ФHJK.]$H1)jhn:='NB%$PP+Q`eP`3$9/%\Zk?UӘ?aJ6fe,"ٔt(]Xj. ]+o),Wq)If Jz;A@_rWte{'8yZ++l=[+TG?岼bZhfLeyL  u4(&$4c}H)1e4dm]-VI5R$j13fU,Ii^ W)"( H'b,4,3 cPS!.QjxMr\*&cԟ yVnmVvy wgb4ԫ1i˳M&@E- X)t,EJKYƤ* o+B6!p|~K#5G_PSB4='WbJ,+DDDIOT Iz:\9nRIѲHMd2]ڦkĪS8IC)/`3%?"o,:u< Ǹ,/i" lY{&3@!MD|ŤdCn6hlva98g3B]Ī@Ejm:#XVI\C>bw^.d-|>_~K F-8h(oSQycyxWƜhȘ8!Xx}uגx HTGDj"l-`[H$;=jbc'k;j/H9QQ^[6 fKz#Gk  7 L I'\ Sg5@!ɧ`9rܡqe˖yz% 䴌yh˳&Õ3 e93bδhIeЋ! s61%R;ks9[W+$bRll}FB|&Ӿlz΄Ehevn) gZ۝ 0KM^c܌5s6k0V1%^ w嵲VkS}#NY9}3RIm=+\x'=A7{\XjkeؓA! pE "E"IJu;z wMLw]y%{2蒂 B!6X}ŁBhȹ`n>BႃB!PB!PrxPK-~r|1X zGh6eT.pƒ:-,Վ_I|z=oofu˱$I^87!(`xsBN%*y\$v̫;˰„~Jg;{J6e0u7)};޿)}iWXy֘\#zdySοűCDNl]sWF\)\G:Xs͹Ud2bF1{?BGՇvc>O 5 %J!| UU2Zvը+ 4Nԝ&$HO SR$2mc}ƌM e2hڹwڅkG_rΪ~Tj:FpUh6fw^f,ӕve՝6ͽ:\srix`փry՛*X4)x▒Fkɯ24~AMZ:ZC~3%픾 aٟ۔z~)ñwEPzaoёoNz [ttk ZvՌѓ2~ ?S$.x;kߖIO;}>&XNuQZU\97s,~yݜhퟸK<ö\C=\W4R1j"~P ܻ@h(וwJllȚ6\~)I?\HM:mgnH%R;Ó9RPZSB&Yt-`&ԀUH!Y%m:X_(75P -yݜhH3le'|fO\~Ka)*?cj M 4v4-mL^$ c!BD||q}Y^ʭ)LLUe-cK*H C.NM<ó I ?=]B`FE<7O淙X=][~~>ʼnsFn9Z]9d#ScyNuRq+vd}KO_pOW{8,2WAh̄_)ןm24_>v0-f{;=f0X Xnu<̏yLmѲ /ȫ;Yu9Xӭ}C<õ\K-Sy ]+A 4u%u3æ;+Z#Qg4d p G-o.Q bn69-u<d7f//7!W9a3@*$+vQ! zpY.9N.?t9f@hfvL:".{M-U&+< |trTeqjپ;5l0'cUkibR,O Mh66w ܫgփr4w>BB.&PƋ\5c.՗ A͏.cIh.f6C\xWcfsXXK\-V%Ъ 3\]zOw;WNecA]1Co%MFW0-f[{>f3f"HD|5{mvlzvM u(QBplfՙ`/O#w#Iο]<@ryIGwޝqգ8B}&&p[})bJ-NK<ݜW+ܭtFo՟iɟ&!Yˁ3QЄW4˳UhMdeqKM{Yl<]z'B޹+7.[sM~ ;]v"l2Q΅;3yB!W vjC@!t B!Bh B!BhL)h!Hts֣y##"; 0h:}W~30 ߰j_SH=]'?h6[..K^_+\ VE{a+cIGT;3lƼ9뮹*8(34oO~+ϯs^:g,ob?gtdCFyG?䚫;/3kן}οlŤ#4 |95Ue"){&U7]O?o v뙧EVuͺ6~T[W74>!A@v+/z ?ߔ^#O:"+̻lB./.~SV5&:[ΙuV=u"Dӏ/̓)8(7 kjNqݲ' ^{Ŧ^#42 󙤐17_^no7l۾8Gb]}}vias%qQvTFcScm;vmݾ⣺DȚ;{X$D/Ok~=*.e}vzI5&>"ˣ"gΘsyАА1_~wS^r_{-,4TV-\0$ϷAɉhNINFnjR9}jDr39VY8CC.>KĄ)v;׬{Թg@⸄ӧ8VqY.ooI&8~d2?p(iX!oꚺڼ._ֻtF!vصgҿ\啙HwT]S{Ï ãqh7sN$%q99S'/Аq\OD;g?ntɎI_}q7_!VH'd/Y$ -5/DGG3O3]q² iӦN8nB)uՃtQhH_ѨUVeIII< %9$ɞnDC3޻M[HKMu~onX-\֭)Ip*#+nH7Q06!O7i z^.:[bԤQkh.(:_[GevBr왏<ݶhSooni۶]hQfVO)<{rN p&7[f~J _9nrGq)I^z5e} Fnjs|w=>@O>;:曍gdf]0L|x;>E(y ڽ%joYG0EZ[ u\|_xсCGz=ٷoBH$e'32;ZNCMӏO6#~ѱleN=ԃy;oN7,_luSYy90 ðva/>#~2b4}Kѣ Ʀ]{M|ؘ{w{$[u)E1Ѳ@^AAP`X$#.qA̙55jjk`W,\ϸ{_~['OevttL&}'}aY3T!X,uZ z?__?#iџooyw«닊{koWIOkn4E%6}?@B||]}n#42 frkтyJ4j~]pl6bywlwcα_le&&:ZXt:/fݿz[X.[;}enGNwƧ+--))kjnZ>dKz%K0Lt6n:zD"햛۶]Hxb]yTz{B,k9ǮZRaicS߸X4{Lm=X6lNj_r]Y~SYB75*W"yoԻtF!|f޽g#JgϚuuwHr[O=w7uCts9=&q+o\ Z58̜>H$ӧϘ6b!4$O>̿FO ÚMys/[ܸ3gTVU\}HD/|z͝={-}-/qnvE.R?|Lڵg_Ҹ!W>*aٶ¢Vz]Kz+uu>MS2|7{ual};{tnƍ-回o-qsp}qAbuyw7D>isN4vB}{\&9 rY8D*͔MjqKGh|}i__O//CTL'IR82An]w0̇|(T*UcS ij^Zm2N\փ$I9Q"a3eoA6nAegj\){V6ByyO=f@jB!?B!?H'_TvK--/GyE-ɓƋ%/XSS?A Iܸ|ɤIi2_~vnJW, ȍt1*=Mヤ;oo[O?o=q2"';W[o'曘s]E[fz^W^yynn^2,kcM,}CVN^FDڻ]|'2mͳOiCަ~~5$~Oju6ffͷ?n7ް n'_1cb{#))۷Esl6Oׅl6[o/t;ncz^D*Hz{'j?_=U D61FSSvQTT|MϮyO\wUDxc?0uĥK};! iYGqk{k^|u֖_ԋmnm]`ϛ@rXLv2# =4*|?Nz^Vb0eԄc2T1;6DH?P||ޘ /oW6@ɅSFرSjimu$f1Z[=XTT;Ԝ>S0eiz){[aaqc㟮[K*+\\Rj%]yԹO}yKGDmޥԒ%W;nvys@.S9˹o5L̜KgI= %Od678֦߳`Aeѷ?y*#ϣ<췸짟:tn:|}r^*R?[RِeemfƜۜ<7eO|Vcn`h1$#&B>yrڬY|IcUjv鱱1}K͸Ga8k9" ~ mmM?"_soTTx+/]zOmڴ+ Ir_lsryOJ}kN.&u_^^䤱—8 qܘ) .nԤ>Ϋh9Ǎ>_zԴIaMa11Q^xڴtX4m>?Kyi`] FC3$AĹ]H <-qTe^Rշ_M.+|򩵧2s ϖ<ڬ\!>e˶[Q^!$>.f}_vȉPTT|T69rPBQ:::J%_6> v9ԒWq\O{OJ@߷*-1=]Hww{mmm oQ7wTи߯tttG]_ IDAT3Ne$rKpLyjݽخ7_v۝n߹ (jwHAپ{_ߵL&S*. <ǟ%!V~rC VroTT 3-5)n_gJJ~j‡;"2챇>7K⫯\sՃz2ebJʸgãS$ށTDZPDE\|( R-v t$%޳I6۲e쒄MB=<wfϜ9SΞ3gS))`ii)yJTl6p,130bࠠoB~w#@.7o}e#E%K,q<22#ʙ0>zWI!u[E_c'Nt0FZ o~wPQYR) ۟`VUyoT\5!'>pk6^u瘑EiܼU>j^okS45y҃YY9/_yl!":&?*'**|)_\U=Ōsܹ ƍUR2TkS]Η_9%wqnT%0q_;cGIY5[MO?/\5cʼ_ y<2}_2r7 |%'N%eIu[eg&$t}S""N>n#OmB!GVqqݵxɛ_Y8ިxe 9ٹ!?c|Mk5DG=|fO>fmԹ J'..op?dY^^['OU_wpꬬ_o7woK-s\;N_\\;㻆u8s0]gvcZIZ\~AQIiٰav3Ǒ#GcdTxYi;UA̚9e/S$~>Ϥb^(yejGE)-W@jz1p@`YXO<>MIex2ӭ[RxXhƕ̑#o۫i:/]Jmۓ'?T@s' O_r="4d@ *2V7.W,7eyo'%yekTψ(5VgɃ:=0լXەȈ`o.~\qepI$ƛEŝ:Px^X)$&&۶}Uss' O_(Оqj:t(q_XU+INDJ_7yekafoϸYDDM:LUv1Y￿q䈡A079K]b^c?<s I|߯;v߳gxh\. ljZbv߸?X,]v]nx'oRszFjZ$'F6Vϕ]TTK ɠWMYeF 0)!37Tq W۫k5U:u .]IIM[xwq.:u[t:n] 8iRd s/_NoHEERxYt)0FUQ^xz!+'W|J|q!}+F:y4~ſ%aZ ͷ?>5ggϝ/᝼YK}\WTV-&ޞϟTщC='C b\C6tzچ[-V0LyEehhFyFXJfrp,2"|]$&'w=}6\oU8,<z"EDGR4u]wlCP M79K<^A8T\RVOZu<>{ܧg[O|{O|,DZ\s]P]$q}7`3Z渞0a}.ٲs'pN++ @lF*z*O}qq鍤FyFl3rx!p#_z>g7xziEşm0 ϸ~ cˌnmlYۋ8ҠmxT*忧N=gs'_ H.Cج -6۷^OK-s\t?ӼK%W*v!~T*}݄^N ; CBuKڲK2]GGE u1n%ϥ^*ٿrQ^Qnq5[Ωk]:yLIiٴďkw[PYirۿzQMչs׃ I)TsOq}7@0e\dSMH߇#GOHhIhh0AyC5OK-v\}穿F9EUT鵱e=c\j䤌daX.T؀}.]HIIiZũS&i|ܼv~?.Eݓc-1 ܼo(#vrrrךhlxv鏻u4MOx{ +Ço}u8wY;#}}oEee@SxkR rK9}&<͛>/q{54 ߯(UeEw>EdN~zD$5!qJK˞\.;;<|ly<9i>4sGfW8Ѩ{Ik[\l̩S  muh%}sQQ9^ؼ=K)*.U$m 5{W0uǰ̬Arzr O|Q}\o4;{>|jNu{R56/sɬժ^n7ER5# \ǯfI%}>!/pG[,KA=ܧfNJ=~~i IK}\;RAڵ!Cn:B$|SR EV[=( $̈́R* naqV#J0ۓqt*KCcM Ԍ4pO|*tꨨӧڻ/1 {e?ؘ-FFn IK}\2y N_ZEƒJ%REt<4/؞x^%vq}uQ7+M7hܕO|nq|{yOķ曢toW_\ԣ{f]Jcݓ:omu~?7=\7lcp<#@3T̥UIQnaiei9n#}xRw& nO~ٱmɲKk֥4܆z.5dj\݈! ziԴ^>tO?q=c6hvl+\ /?vh$FydĉWt?$&ł浳"H4hT\%Vf 2[Ç y9GSK;-o-_ <Ŷbc4RJN+)6Vb/ yPCãUjx|/v|//Zxj_嚼on;"IAcވFD"yŅ~Yt4jՁzsvݺΜ6E/`zXQ.G[3ڐpmC; 'F&+ =yBlx7 4$IfcL='gSDEEKJʌ];p)+'[d:" Ν:\j;}H$O:թCGeN<ٵK[+*.)IM |.]#z/Qn&؉.sBgҩ3gˌv"E:v '-'OY}xm{57ŢVRigۿ|lt1{I?r>ilApVV8+eηL6pT$t-͋,v_{/R鳞V{/3CCB A.]%1.&&+W3@ueee31JLS&>sx-}{θz"}z埿pi֊BCBu1< m}N߷/{&ݺ&1 qjIi8IV[R8yғo۫jt:k4a]t Icrk"̭,GBnEABp<KT -S_؜cO7Z}W/լ/-޳o22N= 3^ʞ7ߏqo-=Bkm'O>r(N؉Ҳkwƕ'>oZ{km{57ʲ̡ߏ1ԁ?s<76Gv&hz8;{tOq?ܜj볻eFaRË̥>hҺC[.;/R_rBQԘѣ=?:v V]NOϚ?q/<;wG*tqo2yƎ޽z/|$==7?=mIu5*$uѧ*z`]fϘ3ggdz~gk{͚aC@bN6r^l=v$I۽nGwA>}ʌFZC+[?߾潷g<6FŃ[`=zR2Sr@Ku]3va^o_3쟩ҫG+e _|k} ~k A]0/~3:svL:]|{ztdsq3fԨu,ϱZmpY\2^-79bD6 _sµnؔЩS+V9< 0qj÷EFDؿ_Aaf3f޸=wd276 /ҋ/WkcAaʪ`RBbg~LIisF]&NyaϤeH!fs TTVw=sE/-a@۷|4sڣw>"sg 0$oը=g鳍Ɗkg}{LJ( 8`0j-oooҲk7;T*ˌe՗ݏ[\lV<|Hpg>1k/ZB,ye!!s]vj xTA4-iSxZB~xrhH(MQ8 M\ibre: IDATvx~3,gm6' Z^Gi*`/o>[L&]⢧/>[5tm]%k7lpIzyт{(6ÿc{$w/9Y-jOh4&%45lp8~ⵤqVsn pSgΈ}cg||lP`D"yl/%8ܚsFy780.Ew TaාI٭GD8~W=>ܸpG? ^xc;L|-\o+ n~՗!!SgbMۇM=Aې-mzK<_Iam7_"6Ax9Wf#Stz77j Kq  ! LMQ;G5iqA M1K 1g-PIcpUm#{Ǝ9Ӏ<9jfd2l`+}ͷ3]_|0LIioqϖ-{P\R^i=p0kVn~?hR˫-|%=ߏ|Jc8<̜/?RaT)k7lQ۾E M}{}Ȉm`v~yϾt;Ə{]~{&$QGe%Z?s_vULV2#p\oZw{H֨5zv;XI6{bh[@Qk13[,fh򗚏]p?fcri鑣ZE{'IKuZmkgAi'7;je_X 4=s\AǦ7z*Po 9BAڃ ¸~QӾ 4\CyΓ    7gAAiP{G!N:A ػQ%%V.)\ |J 3tt"qy| 'u7mb l>5%&tTw= vi-W{ Ć1AijZI_[d9H9f]fE R x1 )8 *h-~#-gڭ8u\*UiU*p jq[  {1^lGu'Ps6^H[U&93%(񴌐m6A43Ya!+ sc.3QcI*daĆ1TEJAM0z"՜Qr%ၫFu\`)3,Ɋm6Ԇ"ׅc88a:.B"#d=UL_dg,.7X&;D\+e erR5YlA:k^HRrj#YbLY$@y_D 4. /]6\Brsp oAnM=s˱ pZH-tUVquI2"$Js.ABtV K]8#.)sR^^[@iIrք3;xc8o8JB{Qs98'K%G`w#\_^?ABbl q ^mǭV¢-H0$TBxRoݵCV3 He8\m1JJHC!Fg9˳>fngx&"-YUJH _^?AOЂ8cE)vÁbÌd11Wb[^&W:Z+i N+[2-,Fcu[ sF:ƿqAv;adC " s|qrkë(^B tK湅Y ! \[ ք܊:k.W]>X|<@cbGI  ;i: )FiD!`;k(ay U%Yg#xHYkh,`B5aQrZY!LT{+R$@UV8ܚP{Kw&]]@`kU$q{0BBd!he4@dsA#Y{Kr,=YZkrD#Orşm%vb',UD:8܂o=5u[2Րx8 H{~a|G[M   -gvYAe} V3  U=   H[@6zN:8/8*7D"y׭[r;?g띊aa )G=z+JT*Z8W nh׃-S_#m?#|7n|kN8pVmH:GQ(:th z׿;)))~O4h0 -[>ӷ~{ƌ4M7? JuEM6O?ݼy9s D/^\3K/vm7Fi~O6 0 h4h"۷\Ŭcǎ?~| H@K]Q`+H[3㣣}'x8qgD"7oA4h3gΔ5p8q믿>k֬͛7?/UUUSN6mo5mڴԩSCBBΝpB0w\3c,~nDFF@LLw _"Qׇ~t&/AVyUBH[ݺuۻw/0p]'''@ΝY2 ")))55s{>RzT*Ϟ=[RRM:u .,]4//O_P$''KT)`0dddt9222===;;[~Dݻw7w<_TT>#͏`HLL050sᅅN>w\``~i ի*GYYYEѣ ==]0aBjj*L8f]7?;vLNN.++;~x~-o˭w?!CJNNσ k@^.]$FZmBBgffz\oAO:/zP ] rCgڃK.7.((H9hwާOAw.kٞ;2l…8kO}#""Jez z^*;w3[ffXŭ\wޝ;w^re^|={2eŋEz 0W^QT}3gN1dȐOnݖ,Yx~qJx)SDDD<Ë/u<44tɒ%*J+W'q0Le'|{]`ALL̢Ej.]Dd=wXeѣz)FqF|-r ']v5 *f'&&f~h4=zxG]-[iy~V؄zУ%kߋ@t=ضmBXjwY\\,ƷnvZ,8p`ȑ.\]fffz۶mW^!ǭJHHw|kfee-[l7n:i:11q˖-ޖQblϟZvlo[r w~e-ۄ6-4T@tޮ7ME-V_#o4lٲ=z\q$IS+**(0`[gϞH$'N|t:;裏wzo>}^|N+..>wܴi'LJctS8T*|h4SN.ZiZf#HLO<9&&t:ĉ=Vn[o 4hԨQPZZz᧞zJѨTsfee5 ntx+'>[XX.IKK󱱚ܦAꗆ/5[Fts_YJ%qUUUog̬Ǐ/]r:uĉ}qI?$v߿ŋ~mظqY֮][UU%˷n*믿+VX,Ƿo.| 6}vW^-ރ?p7nt۶mرxn ee˖OYYٮ]-[h9({nT*D"ٽ{w{/,,p'|2}uֱ,{5aϥKu:D"1 ey+'>[XXs&--MzСCN*NIIY~'-iG9KyۓփU__aǎF)JooUTu{L2eҤI O(IZp=JQ};?a>^L-?Z_g&M񠠠#T"ԌH$<^wekF'::zٲe5g^fM'{+'a ]8i˅&9@V۵ziX_#m~AgogwҥZh4Çg>0Pjy4aS-?fGqcD<HV'lz?zSwekF懦Çgffr׿e=3x+j'ޖ+f eMPwФ#LP"tޮ[X6kB[˲C aYvٲef4ѕ+WzvԌ H+Bo輍 @A_A_PAAAA*ԞAAABAAA*ԞAAABAAA*ԞAAABAAA*LAAA6d@A9Ai}  y=   H[3  U=   H[3  U=   H[3  Udg}h͗A-]7)1AAݥK ݟAAABAAAڪF7CFײ x scEŊS)wѨhX@}\,# #,;0fkjV -eRqUߎ3I^^WkMB0@qD*",R %y~|q*BX ;Uo qq҂*?RH!f'vMdl_Z7:ya=-V4ͽ" 7C B=$fo8#4^a!և  meO B%F\~_R*6EEqrԪ1N)(&a<` )8։ǵ\PkML ÝZy]z'jS,hD=4Rݒ,JF&n "4D0'O(,\i:V`#pB)^j'ޏr⸆eh Wb82܅4 $G y2i+bp,dN3 }AZj B Ucˌj3QԗaTJ;I:])fkө`Y)$4q3MI%n1LQbM(^ty7^ϸ!10pp.]0@&sy߉ }bp n^' iwu@D"8o{N^^vxQl!Anncqp86ZHRǰJXmr-JNkUE * r3װ\Ed28V^R*+)ʍt}Lp:KU Ap8q8Ȇ /\AnLǰ&x͎a%eKnJ%e-$I B À JҠ/&5"쳈ewg? ^gJB\n T$j[eM0`(X"Y1G:r0f4d`H(4eH28~E!0,V4j&rXHRͲ UPH@ gG .(!GCƨU3C4rSpP5?\Xff/xJA0y~BQ)oÎ{YqRVM/y6U#Z8Zd:3J *\A蝞amG[vV;=sQT,a>Ln3@Ix{I˃LJi$2FxbcEKfzMHl)cI6_o#o C 63&0LȡV x{蕞b7իϷk[H\@{>JL록͗ V?2)HaɩEp|1Leˤ>l'7 J5eR*oHⰧOa'VT!U:x!>w\V@NڮFeL "yޞX-]8NZ(A`0LS L08N` Ys-VC8- EA Ё#"_r!1LN92v2Aryމa E|I}qj0La e,[ )D6zL7o%1T|'ImU2  A-{RD)\idDXϒRSk5J@OF LucoVf\"yO h{2"$;f=j25YPym8Ƭ9D9 @  3[#rq1@)" z̺@ QUA@ QUAys vxG - RYTu;m#UJov &mCgJ)@i)fA1lZمe3q|JL)hqs4*{#vZr V禨{֚VaSdV[5^}fJ9B9ыyש _g.b6U[}`eXjwXLv[.#v0n-A*[X`Jq2MG(g);d7~ISWdFl|`~'a)im  s`#cĔj %:$a`jzH湗2/T 7-_ZF+W?jntuE'kRgp;یWရuΔaz\ v6?=,̆uB\$rOޣk2bf^541zuTxѨcOܳ3UzPi4 xF^8fUJ|wN֨'kԳVL/n QPR$BD.U!&IjMVbyN4fWPl%?^|# ))`p d ('ˈ'^,7kFt{oh1}AoTW9֝$QDG.ljah#6Mn֯bO˘Z5ZJ(00EN->\κ؄쬫3i0hA+0Nn+cE,(h(xbk P-0f9'y .(T_;-;j7ꡝM6Mw> 2ogkD֛`= B Iqцo hޘގ!fkԏ " ,-@/YV.4 @`@ b-BO/10 I:B+AI l%v:ʼxէAocC֫.0KMyJŖe/p|PSPk|U70d>I&Ȥ= mX) BXg1즣610x# o"!چdQĀt)Qt eGqS]\F1zqwYR^g\]V"UJdaCq @,A8 P*0$M8v/ESf#-)9t 3GAӘ p $!@S]`\z1֫#IAD/Ƅ(l vAJE JapArhpXJt0ucpДMO.Ny$Q2B* *y 6s SM`\z1֫G6i[FSP j\- wOJ a_GQ;o-*ߪ*(Y"0I b4qB},q1j_O9L(IVRIEGhf-UKlڒ*wcTW;)^}`(z5%R#hh9"ˑ(&ʤ Ϲ8IYېvw,]3|<̎es.:ߝyYqܷQ1lOIo n:w}IW"7oMMsژ!vu@jo>ʢ)Uҋ^}K>F0^o]%$*(<̠ĻM7K1OO1wL+pF1jg[)ݧ'~%YW[ HjNU! 8AvVJ (7A27 @iñs&%0-Xx!} ˈ>z1^op| 83Lr0YTO:UC+(nBUBi7yBvx}['q3GC^2tSS]Gy2Cv n"+ ͞׏!} ˈ>z1^oǶ6)RIB%49x穄c$u21kLA&M&i̚ SNV $xW_VSe֝Xܭx!3p$q| _vFeWZ 8ײv(Y.Hz#JB;gSYnE90̬#/d7\1Gۃ0\5b|uY9;=ls;Zlbt}uUkG)-s.sY'VǍS]P\)F#[ Lwˆ@T3*?3epQ:vuY}E\zD֟ULOx80:N2!@69/ufFv[.9EקoJ+4N@TztwH0{, BoF% M8Iqm}آO݇8a%8I7ys+;v0T.S)Mzz@ *'FN̔slyA:x~~̋ѷal9!/͚,Ғ*\wt/.(TDUI '/S]CZ S]`\S @rֈð0*T"K%iJhFʬ?<1Y%*#aJYV>ggIQTESR.{zjys&: 3QvAoLS4ze"Hy>ںȏ6̢2@q5A)^v.>~*uTreTR5sy$89a:!58`6;'Afl:bsݟ7xfnfú9^-rbux[L=:t'x{@U4MTatΛdx ӫ w TDӌ !}T?;+;g77)eȰAҨ)V!k KsJC Ay2Dɡ%ް>ZDˈ̳r7Ѿ_*g~ٌ_fCOd+~FHryvC }wr&fW<|+˅ oߨ~v)V.#v0n8@ @ H_l'U DƼlQV @  @ T (?V]">@ 򂪌65}j @T *?sVVec'=˲[ Y)~l,_rMTMǷe?8`Ȉ>IC/_ae#O0/Eٳp^eϧAqc?~WhN1.p~Wsڷo=ԥS}v{]E@T?$u$^a.\,(,ɨgUmѱq8v}hxHV/YWo')gL>cW^CsruviڤrQ~經%5wj=eϪ @.|ɢo$~ijvfFw5?v4n4O_񇌬!G M*g~ˬU);D[7yy3g:SRS<|IJ<ϿvZ& =̿O71)Z%޻߭sG(գ_t>zrԴ4)zמξ{.VVV5kt.cQg߁ cGx{x{5_͓#㖣hfcgg۳Gw\gãIF$I6mש] lllڷm-jİ_LLJILJ~GÆ'G M|pwpp@+u'tg{ᄂy %&%K$=y))T*/Z"O5/*::66nW]X&B u748ݻyNZzQΐSBISgST}BgN"o޺獁ujY[[c&P(o|Μֿovl T*DBKiz NlؠGkZo* MQ!!E<}fʤ nn%0_ߦM A*ЭK/OM~v⡗,[|gjZZ=w;qTomZ1 `Ud)ʛĤ?;~W^v=-=RZ)҈~U Ä3 ӷw IH]WN!aiiiir]  po/KW:sV۟1t}6oagg<$J7˖oXJ܆p굊*DhhRn]{;L?ð,׃q;ٳkOrƺgMJRy89:ܹ4M֮EK?'Ϟyzx[a]:uXdYRr2tؾO>5iа|ɤ,?elэ_+ MMQYddn^؟Hq;U7fI\^%<9-[FDEh@zRRS4 w*'VVV4ptpptpݣ{||B|BZlV{=DINnFʫTM(w+F3i ϱOfYv@x5==ѓ' s0 m%-[¥+BT]{БfMEcb223j/w2<9xiܨAaa'O99g?֝d矞:sV8;4-J_LSW4Mo^FsIgd6jЀ;vP5JvܱXːAn v(pZ5]]\$Ʉ:bx73/]T*ܩ3{ x=<,#㓏FL:V@M<[Nw?9l E>I˖,ҮXcvSϖH$۶Ю]YpE.!V<=~\e9eaan]*Z޹cOQd^={n@tҕ*+{'Y7'N߷Ԕ]ۼ~MLlU4fMbkkr\._[Ȩ+׮oXJV_qeCbR+]0>!XIII3}I2+s/wne?gN=gx7)ZzC\^e- oج"\W{3tŋOU>Ճ'>~@ Ęѣ|*[ 5jhZ5 QïazO_ Znǰ-}$ 1Nf+7&Hr+%1Q@&ϳ |CC'(((};YZ჈NJl[5< ԩ8'N׷'[ETE #J]OL>zzV(DFܹ{\{ҵg_%7{/u=y-F\q⭄Eڥymmmm :'\<4(J#4&cd2cVZtI+lɱK-~ѷF ݼaXe\󆭢܈Jڭnnni\\jWL*SݽB/DHHSQ>e?߯yrS9rx>qy/ZG4 JbYYYZMʻ?4nTMPn-]\uOqtҹ}u^ޮefA]Mg亵k8ov)j׶[ K\+ - @?x OXSt*`iӸ[M4pظ䤔[rC|r\?--sæPjZ~_Ĥaa8?|$<<"3u㧫~6]tU*""uT=wi/qʍISf?{ܲ-.Us-l aܫmX.$v1K̰u $5k-~aHpP:~XiƢ|s֌/kΝ5};s֗V!EFS٧m AڴiѴi# ڴi٦uJ(OEqV666Xa"k qT(55Wn5=^% &9::Lbl)/9666b˻?4n0$$ƍ84jX)5|gk6(Υn'Mnް^:eKK\+ g*(hɧ c b7EoRYga^9[wCBdekRU ub~mkkCDl\ѣY~A5|\)rǎxrs)y9Sj}Թ*#l*湖ɤV*~bq痢 O(TMvSy8>iN_p=~:ng-@x{y,\/:ulWjՊ%wԴtv0du@!>>^aa*TՇ;z8;9)87KV.~=&ɳfhr/'S;ܼ^S 3&>]OJ?a;PA:lX>/;vh#%$$m߱{x]pP?}qsׯn#[s /] R-8n]_-XʰwK]{ -6.^\RJT-l߸yH$ zq-W)Ty<|$)9%Ns.C 3S~_/wb˻>DBk"r eReTh7Rs$K)=l*V)9Y)s\HdMSQol+k+!Gly?-)C8eVp7|Ν\Ng/'__Viݝ8Q.a aZlupF^e.s`ogkF+ O{ecG$U6MrN6fm7SaA<'A_.wyL]Q'-]OJD"0L|x+VR^W2y,\0+2WrmSQj#{fD/5i@@h Ax<]JҎzz+0LfV{Yh@@Z5mmmLFc>gШaFayAY]Je_=GPUߗ>}87W"Ma헑E SJjJ0qSPk]O?,DZB|#6 ֵ8Ɣ=rj*6uM_&HC:fd0,^&ep3,,̤i֬2ZRv6%%V()%%,WQ^ &&N7֐^x㽮?x0eܵT&M<9)eZ 0/_2ԩv5UnJ 莿_I͚Jejkk'Í,zb% $k"_Qqd^~c'7mQ ˩]"#-n|3WH2ghGM305Z21g] GRT/ܼu~ýqvi9YJhEK?x,*p r~>[ 6nԠqztCTKKMKOC=f?a"_չx:h4/&7dR̙_ܼu9cw=1:;9eeɣ0 sv;ZNxbې:z2Ry!ʻ]}C_y{;i}{G;jzWtdXZus608L*pMOFԴ|0qv )|>$M}x~Ů];$h(w'? jZҢy3w_wjNO4M g{ 9 k -fb>o/Ooo *C%q?9^رmEQZ$Ξ$.wFL`R_~|Yp{cb_PGmc׮i$TNy#o͚~-[/v%MCh+WowXV#ƎHM@8vɨ~4pڟO13j>tNC?ܻdUL[NDDdUYؾjFD<=k.:) iL# $ax ԨR1|T%-8vZJYLZ F&1jC+Ԯ5|` 7D0ܙӫHe%R- &MV/YJe$7Oѥs_NBXWy˳J{0v^8pP쟘*G 'Jk%$]P(ggtOi1M,bHnǏͳ[%Ը!?J|P ɍѾuV~=ԼY 4nԀaبѩii!{;-ܹ{WU)T*U1~پSTg *OL#&.ð8̲c}l2pS 'HްRH$=y),T*/Z"Ftu'tg{ᄂy %&%\sn{;~B7Ȩ1'_y;ѓ&ߺ}W}-Xyt켯9{ԯç͚7}+׮rC2$7bO|UlwCî߸xhؽ|1uF|a?\4~ldTGfMzݺq?ܱԯuoWU)nܺMZٲ}mբEzF=IϢcuLqIrW (Qaٱ/ Ug\X#a5 }3~(*ƁpA+Sy1 s̙~{ߏfM#?OKK֫[w#[}gfe@Ll܏k֏?Ao{}4 ;cOF}}'Mq\l܋=zT!GLP'ƍT;df\zzݻvxd_?-߼u{AVZ~ִivvUx,jϴCfzWycog۶u+e%4=qo_y]7Wܼ\eFg'' LgdfV@e0Fsp< V"[V %[J߷7sss==h?ŵ?*.ЭK'Fs@߾A@@@T *_Pee%{z/%FӔc9{!4w&MΞ;TB»t=CHϰ&3<>{'s[[wF|2z{“gxm|C2MvOu'4T{HRsQEDzzzrJJrJJNN'^oK,[ф^֭R\rsKa@M4A.* ǟXvs9![4e/;;EXcv IQT&5--ݻd!/_zȖ]k~~ǽheeg}k^x@-9OֵAz4T.~?32̜zdߞ͛kIb. \\9ssstpp>Fy^4wmoOt{{_ub蘖f˂@=XKY4G0yZ܂ x;웇h_`띓-5&-=۲zV0/28~jf4`HnA}j1yyaZy7Ƚ|`(zi-eexd27 O1[[שC{^ԴM[o޶cİ! 0dhy$q) *eC#aLqFh9xOhP^JjaT9nbV E"H+geM Nd`` -h-H-j_cn8o!oE~Uh4⼓J/,Plm쬬7WGOh4̥+W@&vab[%qD~iѿOp?5oL7|LZÆ ӛ*G 罌y`˲=3rVvWtdXZus6. ܕH$c?hBM~A8G#B?xV..dӛ*G M,3?+%J8vثI}hFYP cޯl[yfsP*wfOC`V4h(<ΟYtO0m["oۺCvA̜6uIQPe+tY9|+a>>ދ7Ow!eǣSٷW='G 'CBL$o9jQVZ42ŰN7-'N߷Ryգqcd2ֶUUkz,[WgٿaԴW ^Pެ[^Oq 2,%5u6_pFf:7d+׮ϟ;K*@n^kOrj^qO6Μ6]Yl[kM[ee xAHLLҕ~>wne?gN=gx7):?xСC'ׯo UCneeggd=vkM^zmJO@!g9ۗjD,6@Y_+Sr9dJ*9K.k-,VAN}=n"ݞbe/X i4+wY٥ZM;3PWy1}ʔFc~Ҧ#Tݼ왣^Dt"E BI3`0|!@3ޭ8)xS //pBU nVʶ.B+x3#<B!B/3!B 3@K󴽡UB&4O۳6g !T5TW5 %~k)zS+KiPf _|óeFVk\:=1-3{a/##Lӯ:;I kV!TyusӺEx:,u ,0kxj/[/|n}?;Sl^):ؾm[yddv#j86KmzY;k{:Bg7^OL|k .Z)R0=lSo]E[/aw)Wj]\ϻ y^RR9#: |Yl啎Be@INNWGt_ =;Z/}"5770o(A&b>ʗHd:iz͔wu}Px9\.+ ǛUYR1bJ ie 8ZG1#U)Re\蒋k%>-uVTdBGj {ϯs˫|BR0Cn^DZm+ej߶frcN֜jVR Q{ݩSkgΑ5} <,CLW)8UUO؟v)( G7''Z.Uz}dV (}PfqsOH#U|#PU呑Y# ?e.u }dZ)-Jk&ܯAJN5*tۖ!L$<Yu٥UFzǴ UvDYJUc{R˒H+C┓P |O"kשQ hdRa3:Dc{HWˇ`:??SsU`pڥ:8j*bqp 0Ia+C $99##3_"Q+k(+8@[bxyNڶV+ZH+Cfp>j]h)h".lB:tII(mY-9:d%.:PZ>!TEe*?M0MQjSg}☓;i`yq[UlS(/֏uw#v,R..B6jV^^R!8tg(jX\ળˤܵZ|յ gĔSOR"x`%sRo~uHXȏgcWW)VZg'd_6 QsJ啎BUթ=oޚtoǞ۵q("Qd- lOM\NC1:gÄ֫c|hy#T=)v=tPYW|4"6cU(?%(-zU")R4|WAUJ$ːa%J}1B-Z-ΫE# RmE+*`Ϯ}1/xt9hOl~+]{)Jhox=JGRhOl~+]{)J^V4]'2U^U7-B!B*EϾ$ B*ɸB,?B!?B!I2Tv}iذ!ܾ}%_ao,@*~Gk֬jχ~x'O]J!~^DI;zK.ݻwҺu* !T}Ur/NU؟X>|xvRG6oo\ڡCbһwo__:v8l0\Vo~ƍ~,۱cUV5jhܹQJSSS#""nݪ1ۛ]],ݺu87o͵ggUV ZmDDڵkju c0wʔ)gΜ!geeիWoذa-jӦͭ[,m_~EŒ?ۗ*p.U;eU78ެ::thڵ?ӏ?… qT*>}b3 IDAT:kaB>|x~ov}n„ 7n2epy_^233G5f̘VZcƌy95sԩgvww:ubxʔ)}т X5kV411f͚PN7|<<<]_]`AR?*q픭STYf'Np ....g0bbbRRRŒ7Ҭ|J R*!!!@5jڵkܹ`X!]@@L& e+]\\j֬CKSRR xbqwwoܸ1!$22t???ooh[5j׮کS)S/_rJ AAAqqqQQQj^W֭[fJV/yy;w ۷o0ԢEO޽{WHqttlԨ>xcBU /Xj_*qͭ VR3ݻw~m777GA!00u֔-[V}}}>>޼yLW?J֭[޽{Ɓ' 0`֬Yu/fRpT卋MT*v1 ?SάYz!$:88 1ăz}۾@%}gϞwM>xxƬ'رp#Fxyy]eɓ'߿ĉ6q)S=tжmaYvܸqK,|pvv^lH$rqqyիݻwgΜ)!>|xpp05*--mժU2-Tݺu?쳍7 ===SSSM7 |/9֭۹sΝ;sн{GY&$$IMѣG]v=u<~xڵ#Fptt4 c)U}޾@e>nI{!?Sqf͚>L/^_~E2Lĉ֭[T*Сd,RYn];;.]tãvRT$2/^(HfYϟsNXӓa$0ƛ` ,Z[)/ 5k(J''' m2^+:tp͛e"M7qN8!\挍0mРСCϜ9e˖2Wקt=***<<< @ReddKO>-|Ǐݭ,SB/2S6iOQ5g5k֜|aÆÇM.e˖Ǐ3nD.d;::o߾׮]NLL,4g/yٟMz=BP_}ڵ4fZRtB *q MST a:H$]\\!˲fǏ9rZ 29#$''3 sĉB]pעEBWP>}joo(bqBQR>>**J8 R%c)PՃKۗx~ve3ՎNۿ J%qq̘ hׯ_zU8<^z75c Lڵ`&L_233 ֭[NR(K,ffΝxߟO>Yn0&xʕ=nK۷K3g\~N۱cG c`0DEE-\P,g6}-\066ŋ7L&TT*=rHVX1s;wl޼yرk֬1 +V(+V8p'OJU^os&<<\`E.]F%nٲڵk^*,2X!T`Rlb2KN٪=vm˦T* MwhR 9rK}X,XDޞ,p\ e6ar ͟٫ .#@t~3! ˓*T,]B{yPÓ~0oڰz'9uy`&) }U@?BX$89LH121 L_ #:=tiS@r .m7VO|:;)ًDiC[4Q1af`OIe $R neNa;p9rad$WBߧ-B%8LvE$Rx MVmfob|npq'y4O7eB#9:sg۽~x_)] n<ˀX=dc"cXJKD tf]h@I)p5$7'M'6Gq5B)msڰ.MIWIbv :)Xu.YcoH=\ ) !?3 2[iox:<:=?Ae*ܼC"PkhєMQ;m@զ)MFT!I$W$:7nJКWfd+HL,xCU;b@6ЙIN#WCIFRO7$=gRowJPLJvjM u.:nsdk :Sߊ"IRx@f6dT2@$2_2jҢ KB<&*%7oAP#ÙÍo zC w䐖M Tެt%N.W ! 1O7z R >4mDbHJ!Z=] F  zHDDr9}g L Mޅ_aTNMFԠ% mуws€O ضܐ/atia;CLF{w-/1A^i<-(k;~2F&Yqr dfK[:/r3r|/A2eΰa{S-oZtd^ҘXw@]_qb'| R<\ >܎"~guzto2O |غx@4 baxt=5Pr,oTJd ,db"'X2ҥBd(%p<;j ߽+uQ9yf\a5:prg}Gp͛5M>J)Dc:}KgѺ5!,N_b;#^*3jxPM>d"YXy] :~X?SLN.QBhܖ&!wz;ѕ OJO 凿ߊ0F HxiD)|k||ZY7P҉DGo5ۥ VOڥ 3S̎#,5Y9Gy's"ӡ%;p87mƒn7]篱3_Z99SV*S ID&ނ/ NĂ^I:¼ك*;lD'{hބĒd Êwhio<>1Ypuz.4GC×ӗLÉE .뉘:>0gUIUҽ@J:=L*:{1KX:ڳ#ٴ]]1<j ӗO3g0Zsv/J 5kVG"cXga٘TBDbŠ*Hτ3awINmZ5RAeR\HNI A*:5)!8ܝ)H%T3s6OaYjz (!zKa_ M-or@. cT>^t:8q8'r9AXLN ;;| ˼)BUQrd1&[Ma@.1U*h\ e<\8؁ H5@"" !Z}dzHI<Y$ -4xdږ,ԮA@-:b&xHL!@[;:Vq $ՉgYM>rRy[6㏝cȷ+=?%OY؟A6r)9܊ 8h&{T baSB)R@@&+11s&oM$P9=[dbsʳt<x ,K_,bpuZ-gUgd (N_A`ɑZ*o=t\tx._+cC-P|b0S7'n!6_Gs;fՁ,pPJjFKJ!)IO֥BSHJƳ٠@,1 8S`p|=8SnFj&H@,z6]=h!=S((Hx ddZ,X6p8{Y`\`cEl 3ƞ&߀F#Yjg'*c8?∏']8]3p$X?Td$Y eaH?>W >|+5_^B`ntο%rrVyZPuÀ޼nͤCNCN.hӃ,Zٺ1X}HRB~ $= fn޶x ZQVnfM"kQ/ ~6c8ax{odR~X[F#ҎJD!#ؙA6g<0, ů@8\N_# 3x6sD|qgwsAMoK9M]r۲-/9(V[e ;eIYMK %#LIVW.BрJҾ_D 0k!-H%D- j z:ٗ{:@*)xL 9s KMRyչ yؾyah!.w@l yq%ɔ$gN]7+7!ܜ 'eYQP@,Ecŋd̆QJ(KK!Q(!Bʪgv,;BUE(SqBgl B!*+ B!*+ϼB߸a7sPԤq_-e=YhX}׻B_߮zvv$&$~)Ӕ~,4o~.B@խR7KU&t{ܧߊ49}y\ ŋԨY&!` !lxd= Xo&<'nrӥZ=C"d~v3~[kD=4]ϙ_i0JbIx ;AkW_x`Jg4y fk@ok5͞ 8~tLQ:H%.Tz0[.+Pz3_Fq8wP%\ͅ3Psu)|<轘21 l͜ .̜ItXD'ezpq!}r,c`|mo:s!C)Њڏ[=p9|)p3WB@*~e p&1؊# (f` @Xޛ\[^+D,n=-z(mݯBtҧ\NT}٠+] ݺW^LYjxPtnK!5dh-{+{Jl¼6_ܾ VCZ b!1 <-/lo[h_Lȿ 2Gs蠉)=:?U7m.q0oyWXLд *h] SUM?=2yWϙ^SUJcf+}gՏxN">%Mп0ɲ_{m:oSs~`չ wjsގ҇GnKx{!Q$"s9D MKv 0 1~6u}te_*;Mxک <ݘ)1 ;?{U9,SURcf+hBZGzW7 *-WWN45V8F-g9yŷ/o: ςKDz~ƿѵ죂(2ώujk7frV^2SUhSVӃ&T٠?3]ߤ_3':>81!C:jϪICʕ<U*O3ߝ6־JփTB̖J=@imf0ݯJ(B<Ȥ-aE i_< ֫EaN\(-N0?}m8%ǙqrJ9xMgsپL:CmM#WnmC#-{B̨MTtYgAR%A7ВTPzsW!S*w:tOyj,xOVO()SBH 7Rٳ/rf.h4/(++k >|؃.:}QcĿ_$!TqԤ?O^|S۩zD"Gܲ;~nAZ5MvE߭krb|988{vܲi@~us˦][7El35O9r:俧sƎz7Ul޺~7%Nݸy˷sZKvoݶ&$6lIIɟ~w V-[@h maBg6+C:BtjMsrA(Nشe[9lj`{7e~PX$J3{{m᫩gte+W{z/aqӄ7n@`nj9dSRo!6gu[ҫS9\zy>v$o2{Bbbhmp+4|? hZc7n&W9v'{w߼գkgX,_9r9qĤ$*fl}~uJ̢دѐ(zut1!}]ǏoeKGUȂU앐rhj۫ɘ3YSHl |R|C;Tj֪e w77*}zۻ׋Gh"ԙs:^sKoޣkghԿk==<YsV*Q{ѹ6>lMq qO#F7lh*鳃y1$4Ԙx_m>;v?}:{ܸR+"F y4/Χ<!!f̎?&]h”fM]aYVp.1 sW&%t,Ճ4Sg|QZv;( x Ю-[a<ϗ6ݶ%B\n]y(33y`1]V8n#G Qco}khӦ7n$Q勾0Z6o% }}ի|_h”;;]Ḃ}[ءZh߮{AR=O/UUiR4 \ x#(0aҦ۶DP%xu9tĒNlܘAMÇvuq}uhqV << iӍkVTJh~Uə0uNqtzm#v{OM[)wWLIvm^R*^VVւEG߻-@rJH$u+~7qRZz:qJwsuuB%ҽk癟eu#$Ęר{YvޛG{oߐ[aвEPԽ{IIv&9p'"rUMUJ-מ=m;~]QÆAM,]fJ=OA7?r՗T6cޛ!斕e06Ifl\ٙeRںpFG{`Rr\"''c OYYYp>zbX?z͐윴rU foڦb񇓦qFU[ !afdԙB1|ࡃG`0 >[^/gtsu+MuB%ҰAl:9%۶3.[ÇƓCGvur)!]iԮݪEs{{R3 bIz~Cc\$K4Bԃ+.e톪ԙM^]{w?xuH$b*_~KSRS5yLVt[ !gN>4!i^s ãy`|7B7u~eV7lPΎBf)xux{ߴ>uƬίupw|TR(z0[p#M?pcvf3TRe-7GDw7,[<~A(@tʠGn5~Y];>rTX$.`0ʟڻgU?YHo߮ͧS&E{K)%@^f)*Z\|?8t䟕?~ŤddZPN+`>`D[/_17BBn~}d7_}~#B !NNNIII@tUWRzo]9wcǍKIuZlT*@ɟp˖ !>={*DtIAůQ#{T?77R}~1sFÆOt_gLRQ%g'ǫnD"n[_"<""#<$b16!#tiO@ίѧ?cAqYo\.7ɟQ]VNBT,DtZ"\]X$?yz=XEKe&}UUIlӺ%ttZ/!1A@i*x c=?}k}vmZk FBJfVNe tO8#G ozRw7:̹sPzJ% |9WU-מ}_̬]{l$;tSRSZ Tt+YSG]VZu5T:~迏R%L&d H$\fs"~L49%ݭD"ڹ3Z]F3ow\U-אA۵m Æ qusuJǏݵg_*SgtL&ڥc'g-a;}9ڢyIM$b SW\skٺi^o8ϱO-`ݻ-frCҀnJR(vlyB!f"SJ)HnH$0y7MjbLjRPBp`XiiB.WT))f7X٫T0kY!ydzT Ø&JR^_elvsuVM_'e ÚNnTtObl_lyFhoy~Nọ̃̄Nqm |_UJei}`XWPԿR4\n~Ҧ#*p̬74 pwpvrۧq -hU5dՖeegJ1[~EO+5K+k4,m:BM%oVZbE"89::BU^gݳ`싯323mBU/?d~<@i9&M[D!nGD|9w@j<B!?B!?PuqӶm}CKK=2:r7 7}o֩Sˌjx_&Mʹ*O^j;+>J :~^75Oah]>Xt~\Mysbx'SqzSծSC=mT%@yc׾Woh4ڵyo``J+uR~萷;vhk\%wV ;h зu/P\ ѥKZE|ϯóݸRLNI{MN^_Ӭ79a`[\T!%o3oN}' }p6l$0"x+vD_,;tm۴H%=ٶcw||CF ԦMK\v;q۵m av9pRqҦ\PPGoWp^~vm[={Vڵmlj-Xn./Y^oQ*?߾UY3~ݪ>n֭=g/<\ؖ$e }ׯ]vZȑ۷ظg\|mӦ~d*3[=>"s~rvrjܸaK%(Bh4V-w+N +W,**Ns/z\z9RT&}߃ɜL o?/֟[ KL3I2 :iݼ}\ï _Ϭ:o=x+VuP$.ة?2h{QC,^wF!Tn OMOݻCG 0\.~3 ]9H$)OEWYw}*ryFQ}ݫ H%BJT ( ((H5 H1w.!a>$ٙyn>#J:v50e`#cC ú:Udіp%=؇ob+פѣ[*gqUUe7nMLLM4bCC9l+t9[BBraaQ0ťi-VaذSL&&FcǼYs'Nkmeamekر# >~5Y#DK$u{5FEEk~gq'Z1/<Q%/)+qn|B˼TT>|tjv̅{e{鼕I2H$M`_ǏW`eE<лn",KNF}oV 0  afj}} E&}tssM4xr_;_P_P6!ӵsÆ dI{g.Tݫd=5g``|}0;點h?x{yv ww ssý˼tqj:5uuu5jxv!:o%t.{]UU]SM`۟a02@Q- ks Ȉg2ii?[jOc}pi _ IDATY{9mws/))TUMۃO@bbrd3x᭐ eYUWW B)ŋĀ>pƝ(VoFz|-t-5-#7'Wno߱x(pUWW3w=*~q<&yTTtq.Al܋-H70beEUtt_gr&|}GyEtڥy|PFgHHT8xv#c0ߝ0nl''Ǎ|ۍ_y39;|/aŧzh6D|Bw+)7Udyk@/v j$I*J(VWWcnO_^f;8ތ<}_8dpgv6w>dtU|GMLL-፬,|P(dzU% (o>f/^]<}CGO?YЯO/F44x}4}ϝNdlZ5:ϴ3U_gM@~1ibX$GM\X\TJYG(ڳC ^]<:}V8`wmr&]SSlWAcFTj\ɹCBB<|[RRjjAjjj@IiH$$"5-̙~bg+(J755iFJƍIɩC`٠7הm۞M?ަzyl{qΙ Ԍ[ nyGGY3zꯊ7y܊K;2`ɒF ?]j ʾ^cK1TVI#=zoW)LDG[\8>ތ3g/^~b^̞9ekO lj#EQYY9a{!N%lٴqx]/(Tj\--5ݽt |fffjjA19s<VUU$aԨakmF8k5#%>xN^*Am=o^cǏ?ѓ 軞h\&?qg玝=:A<\-IY{2Cf98~+ cƎϜ,kvekXRSSnkj%axC r]QMT~9a`%:/(ӧ's=4H&ge0fFG‚" =k`ϿϿWrsvBIK,((׊KJڲ ׊7n߯{h( ٹyMMMx|T*Vo^b;$& (ݬ9/ްu,w=<~P*)"ԧwA]eTޗ)7B@S JdGst.{o ,"cXb|H>FX \싃͛w9sxmf4!+3Be,$RYrSaQLVovh?y$S6k֖MՈ~HԉC껞Y"5 Q]UPY]}{~zjZ_ :v\r|#P9l:m0)U^PLJ#US 9Mg#Hh~0ӝѭ;;3Lsmm|Bc26>c|\~TUWSnN_ٻGttl?%O O*>83#L&5O jGL&v&9l>' < zU%'_3SӒĤ\QD3WýAY >C̅zTv_N?~o&}:+663.&.\oNgñQ#y< Bx11ϧN ӎ;"#3;7'4aW}}0MlUՔ[ZjF~bbx]ȱ̡rKMMM8Τc.\<]'y^I}z?L2X"ݡ݂ Pwĭ  lFj=X*$`3ѣs }K-^?i\< N:U-3LI KQ[fA#4g_N .70໺8 6Pyj{prmanp-lӰ9Q#>sMM-FF|@ZU1")gM 塕8{bnnޜyKCfCBr"ٻW(@[]Wz^sfM۸i>pb>)4Mk X[Y.Z0kvv6jw=Q:99'07(Soyphz=ktB*i/=^[]jZ.߸~J&GHXMud8FO_Β+ank [?' 9vlܽZqrJڅ(OwxwN!}yi2s̘@`@RdEEv>PU1{PZ#߻9$plrG6[6S(7޾ +T=^t떯i|妋sG 4y w>y99yjw=Qe>2,"Yx5uz |:e ckvIu[FNLWD"@|68<ӯfhDEVoYY14*i7RDMmpkk˰$I(:_At ;~7 ckvI>zoPϿw'ú++*ggWZ"B$m۶G࠮2<#3뫵브.]WZ6Пrh$ CTm }£LIi|ky!@  @  ϼ$1)%2o@ǭK|?S^ImWyTǦ7#w?hX8M4_N6-]}׻h[n}žEG5UC=>_Byl}̜c#G\ @?Gztb^hWHYU`֕ ObeV .2#9jt`ĉ7uru8`W],mg" ujV&JmtꗣzMkRQYշO/dm<,&cm@|9WVG ceipnn _,& x <\ wdi9ps$WGtptҥy֊GQ4ʗ*]MU<|H*y+OE<*,*l6˭sqxDUUupP2=4x';kΞvh#I30ʅGOqwόH8GaH=O&ZAQLF1( LB+<2Jk50kffVaQy܋x.;=OJNS.0>?D"a?Ϗ|UXTĈIl;8(0#3+6.y֊X[xw1<&.6ʗ*]Mu r8 S]--۫\HLJ/(`NDz^FdhXYU)HLa&y_Ŀ?aسe8r͗9PVV(<'Ξ;_?{(: > govkZ󵗯^U[Ȩ>]tيw1|ՔۇseeTTT~iˮ=<"8GD>yqAaNLJY̬ʪK?y^WΪR(ż|F(Yttn!?<}MQz h ӕ~g':2i SFcJ]WHO?;shoiΟ3soLdgkiqHP&N\#4ǫ}1Oކu_uruA!c&7WWgg[6كUK~ڿΛA=w(*0kh`+˺GN.P$ڵrVHb{6֌㸯aC|}pVo#o&:lsً$q _&F8=O:Y}dąEIAyՇf<ŋ~>{NVV7{&L? \dJ_.%I[L;잞A8;uxr29>^^ХǁG&GUԛ,7zW93qc>ϙ4:v岏ߺc <=Iir|^G,K*=xVXĤ㿜ܵc̹ KJKIJ07o,  ^!@}K*$ّ={ַFM%I\.x;CF=~ IIYw_?!8yHIMnsf|hbA >[_p"DSg0wppd)IiC j[Cfѱ3-gLSWz}?=?i˶?yO3CYKw Q(\gެG íӔ'-Cv^T\Y@ @t֛i<[!+jjˌ L,UJ >νp֭+O͚^V㴌̜aC &әÑÇssuf32"DUVVkUKvu6=`YR?TVU%&TU{꿗槤)5^:dqwX8o'K-7{@P(HyeUol2 ss_"hLD C6,X 4%% Kus.jdd}.]Ihte݄^n!9yՖUv;|i.++Vo M\/\u t]y2a`,/(zlЀ|>/0ƭ;SY BH,,-kH򥃁RK}gOR$][n'5}:{x`r|iߨSf)**Yɒ' Tx \$iiiikcckccll|엓4]g4^:$+;nBGv6BHΞY a&&&-AM$pؘ @Lޚ@KF;lCFBPk,, tG,ݰq-̷-(Z0g3O+SrB_acmMw5U4PՊAp8\Ǐmm8l6h#Q^zR W@Z&Sv 1[ڝIDATP됳3ʊȨjrYY9F&&gameit<|>oՋ>^|[1WO9ph҄Xb{gXq?M.j1 Sv |$e2L&OLNI:{xrVG Ntcj>@Yytm_¤TyA1);TM+TnARÚ]`@vco=d2wH$m4Mv(%Eϟdo kL[\\R|؊|3'I㿜 ڕ7qbT ek#ޗBG$uNu͌ˁ# _됇ù\iv499O,?Ϙ659553+Kl93̝1 z~>̐SUeemm[RXTdiiչ3ӻ7Œ[w,?K273c|"?Y:wq|"vqv*++7,:NU[=6n*Jp竕ce\.gnz Ⓩ~cׄV,32L8aGcݗd>{efeO1f 2x$p7pA{ZI?n=zC(:҈ChAϞ![x}ذU>9r<`aŽ-H&_qǫNUV@EeW_oT^|۷Hһ޹w$w䣅|T$2/V>}sM~G ^xjtSƏ>`ŤqE  *WL PaXƞY,*iv SeUBxWEtek⪪*K **+i666j`L.25}Q󞗮V(%E vib_ޛK3c7P+To@B+Dpg9 Ł?) iP$bXo~h2+nD¢"Ќ 1+/hTX;O-JKKJ󞗮 ,8|>ljv@tWQ]~?hG*][QO+*+J/^rоCuJ  #CCNqh?ߴmu9.K͜[TݞWRYUIl%j~tNEeVdry 4y]G,n:ҞZ6uu&ƭ@2Š _hS@Zg^h m/5Y$'30'pǩ|.&YEs2b$3hFf\G Kˋ kq]gw$pGltS lnJl5};$U_ŨR }CXwwdp~%>Q:3!yK5֚:Tr}-WǮ:D52ݐl"p6ц'a.*jq5+:a yLMW:@iS="`ߣJT7h z{t{0 yG }|6 Y\AbgB`jz6mftcg$;nEƄܮ>t7k`"hA8Muc]Z O0p@P:sprbaq%WUx&r^(ޜjcX#ؽx KgYK,$N՗VG 6 Mc$NIOӘ!.Rt`WY+4δȈ_*<ϖyf`~ L|ǽAwkr@ ϼudt̡@d]iD>"<1[. }neI%71ېH':@W,M%2.sX#嗛9Z&ikko^4&FۛC!ެH'zhEP正g*HZ)f*2ɬkn^j/'p:Pp1vΰ$.Jo!06W nىyϳ;tOcD %7].)46.U>ִvn6Yt+uvgvܟ!)XAO:qIEЕ@/o`W+1eGKcnSD<b]mȗE%2~K5/Ƞ#Sg; 蚯[@ %bA| pǑ}\Xi\^#TָX`疛ʸ2JSa{kfaX.=PG N{=3O3 dVߣ1MuTY\',ͻXZZP[zxzVG ʭ}:?²J, Q6+GcJʷlq7ߦQڻ$aMS{_&)<6󜎺ԩSǏWOzO BP4`PgMj1;մ!h) !#퀞:& cuD  أ @N6\ <M\MYnr6K-U4,+hWQ4?U=&ë颴Kc{XJG  MzҸ'dx5]{i 3^X++xhS@ E& M $h~@ @UP@ @UЗdW3x`777q8NQA7.;o޼{JR}gΜ9/^j,aiR>ڢ*.iӦݿ?))ɳNNNB0&&FE m;}ۯoZ.#.h~=C|sYήݻkObb{ѣ?o߾> @7 z!cǎ=zt֭3gp8MOhh(kӧ=zСC .j"ڵnڴ)$Dݖү|-^O>a8;;^ T]8|&j^ě/Qn+޵]FuP=3~:|Ͽ{ĉ\ҥK i HKK{Iaa֭ĉٳg:th̰SN>}֭[O2:u%KV\iiid5}ďa50 ^zܹp;w???CCC33+=uԻ 9ٳY{UՖJKNN3VVV999sڿOOObllT%MC @ d_4i_P][{v@L{ŋFR67$YoPPMӾ J̙+Wqu5o 6lSSS3e\[l زeK׮]է_Ux)S]v̘1666WQ /E"Qpp… ߈ ޽{ƪOu뜝;tvZHv)Sۿk׮m0nccn:HDӴSRRRUUs,1110 ,9r+:vz޽{3a bggw-5 ,=z01Jݳeii)EUVuqŊgD###??q <}:Ⱦ0Un3NIKe-gN8!mѣ?#//я?ҹsݻw++7n 0 ..RRR?w'N}\v 2227y뭛+Ⱦ/fػVˈviϐ$w˗ovS<O,7ʕ HݻwWnP$¬:trY,V666ׯ_gݻpLdkɓQkkkǕ[^xEyyׯ_qF$g͚&P($P(411 BBBΟ?9wtuyڵǎS61k$,+W0Ü <{,;;ˌ*U園VVVr_~ 111>>>"ƍL322,--_yPWB zP6޵]FL'//o޽W^]nݣG.xન(...,,իT)EQ,--1 k0knn^SS\eee)))MƯ>|nnnSjchhl@"7Sت´7 `֬YH|>ǫnA%&&:4<|8m4f6Ԕ'OΘ14beeqFgϞM>q32a„t5.چWS8. ǍZ##SԨCCCÌp)S(O;wn;v'*]0#s}]= wYhH$Zd YuIk_mQS)))|YLLr /'''!!)桼PWB &hK[i_޵]FLE&石g $Io޼YiqjjG֯_/J#""?~̴E]zuʨ>Ν;3[Pu[n̞={Ǐg_v`ӦM8aÆL##{)][^p1r/\ ayyy;vXlYNNN\\O?4cƌ{*;v4ʯԶSSSğ9,PCXXԩS{1sz*_ EPhO{mweէ f8uTk'/ByE"QUSL4il 8BP0LxU166~}7|>ܼxp7R.[_r:]U~rV&~cq\UMh*_Vٗv / ڗ֕keD}AѥK!C>|XN@ ^d_Ⱦ }/hBѻwoBaÆN@ Ⱦ !B @ }/h @ h @ h @ h @ h @ h @ h @ h`?@ hC?@ @ @ c^DԃvIENDB`go-pretty-6.2.4/cmd/demo-table/demo.go000066400000000000000000001170151407250454200175140ustar00rootroot00000000000000package main import ( "fmt" "os" "strings" "github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/text" ) func demoTableColors() { tw := table.NewWriter() tw.AppendHeader(table.Row{"#", "First Name", "Last Name", "Salary"}) tw.AppendRows([]table.Row{ {1, "Arya", "Stark", 3000}, {20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"}, {300, "Tyrion", "Lannister", 5000}, }) tw.AppendFooter(table.Row{"", "", "Total", 10000}) tw.SetIndexColumn(1) tw.SetTitle("Game Of Thrones") stylePairs := [][]table.Style{ {table.StyleColoredBright, table.StyleColoredDark}, {table.StyleColoredBlackOnBlueWhite, table.StyleColoredBlueWhiteOnBlack}, {table.StyleColoredBlackOnCyanWhite, table.StyleColoredCyanWhiteOnBlack}, {table.StyleColoredBlackOnGreenWhite, table.StyleColoredGreenWhiteOnBlack}, {table.StyleColoredBlackOnMagentaWhite, table.StyleColoredMagentaWhiteOnBlack}, {table.StyleColoredBlackOnRedWhite, table.StyleColoredRedWhiteOnBlack}, {table.StyleColoredBlackOnYellowWhite, table.StyleColoredYellowWhiteOnBlack}, } twOuter := table.NewWriter() twOuter.AppendHeader(table.Row{"Bright", "Dark"}) for _, stylePair := range stylePairs { row := make(table.Row, 2) for idx, style := range stylePair { tw.SetCaption(style.Name) tw.SetStyle(style) tw.Style().Title.Align = text.AlignCenter row[idx] = tw.Render() } twOuter.AppendRow(row) } twOuter.SetColumnConfigs([]table.ColumnConfig{ {Name: "Bright", Align: text.AlignCenter, AlignHeader: text.AlignCenter}, {Name: "Dark", Align: text.AlignCenter, AlignHeader: text.AlignCenter}, }) twOuter.SetStyle(table.StyleLight) twOuter.Style().Title.Align = text.AlignCenter twOuter.SetTitle("C O L O R S") twOuter.Style().Options.SeparateRows = true fmt.Println(twOuter.Render()) } func demoTableFeatures() { //========================================================================== // Initialization //========================================================================== t := table.NewWriter() // you can also instantiate the object directly tTemp := table.Table{} tTemp.Render() // just to avoid the compile error of not using the object //========================================================================== //========================================================================== // Append a few rows and render to console //========================================================================== // a row need not be just strings t.AppendRow(table.Row{1, "Arya", "Stark", 3000}) // all rows need not have the same number of columns t.AppendRow(table.Row{20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"}) // table.Row is just a shorthand for []interface{} t.AppendRow([]interface{}{300, "Tyrion", "Lannister", 5000}) // time to take a peek t.SetCaption("Simple Table with 3 Rows.\n") fmt.Println(t.Render()) //+-----+--------+-----------+------+-----------------------------+ //| 1 | Arya | Stark | 3000 | | //| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | //| 300 | Tyrion | Lannister | 5000 | | //+-----+--------+-----------+------+-----------------------------+ //Simple Table with 3 Rows and a separator. //========================================================================== //========================================================================== // Can you index the columns? //========================================================================== t.SetAutoIndex(true) t.SetCaption("Table with Auto-Indexing.\n") fmt.Println(t.Render()) //+---+-----+--------+-----------+------+-----------------------------+ //| | A | B | C | D | E | //+---+-----+--------+-----------+------+-----------------------------+ //| 1 | 1 | Arya | Stark | 3000 | | //| 2 | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | //| 3 | 300 | Tyrion | Lannister | 5000 | | //+---+-----+--------+-----------+------+-----------------------------+ //Table with Auto-Indexing. // t.AppendHeader(table.Row{"#", "First Name", "Last Name", "Salary"}) t.SetCaption("Table with Auto-Indexing (columns-only).\n") fmt.Println(t.Render()) //+---+-----+------------+-----------+--------+-----------------------------+ //| | # | FIRST NAME | LAST NAME | SALARY | | //+---+-----+------------+-----------+--------+-----------------------------+ //| 1 | 1 | Arya | Stark | 3000 | | //| 2 | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | //| 3 | 300 | Tyrion | Lannister | 5000 | | //+---+-----+------------+-----------+--------+-----------------------------+ //========================================================================== //========================================================================== // A table needs to have a Header & Footer (for this demo at least!) //========================================================================== t.SetAutoIndex(false) t.SetCaption("Table with 3 Rows & and a Header.\n") fmt.Println(t.Render()) //+-----+------------+-----------+--------+-----------------------------+ //| # | FIRST NAME | LAST NAME | SALARY | | //+-----+------------+-----------+--------+-----------------------------+ //| 1 | Arya | Stark | 3000 | | //| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | //| 300 | Tyrion | Lannister | 5000 | | //+-----+------------+-----------+--------+-----------------------------+ //Table with 3 Rows & and a Header. // // and then add a footer t.AppendFooter(table.Row{"", "", "Total", 10000}) // time to take a peek t.SetCaption("Table with 3 Rows, a Header & a Footer.\n") fmt.Println(t.Render()) //+-----+------------+-----------+--------+-----------------------------+ //| # | FIRST NAME | LAST NAME | SALARY | | //+-----+------------+-----------+--------+-----------------------------+ //| 1 | Arya | Stark | 3000 | | //| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | //| 300 | Tyrion | Lannister | 5000 | | //+-----+------------+-----------+--------+-----------------------------+ //| | | TOTAL | 10000 | | //+-----+------------+-----------+--------+-----------------------------+ //Table with 3 Rows, a Header & a Footer. //========================================================================== //========================================================================== // Alignment? //========================================================================== // did you notice that the numeric columns were auto-aligned? when you don't // specify alignment, all the columns default to text.AlignDefault - numbers // go right and everything else left. but what if you want the first name to // go right too? and the last column to be "justified"? t.SetColumnConfigs([]table.ColumnConfig{ {Name: "First Name", Align: text.AlignRight}, // the 5th column does not have a title, so use the column number as the // identifier for the column {Number: 5, Align: text.AlignJustify}, }) // to show AlignJustify in action, lets add one more row t.AppendRow(table.Row{4, "Faceless", "Man", 0, "Needs a\tname."}) // time to take a peek: t.SetCaption("Table with Custom Alignment for 2 columns.\n") fmt.Println(t.Render()) //+-----+------------+-----------+--------+-----------------------------+ //| # | FIRST NAME | LAST NAME | SALARY | | //+-----+------------+-----------+--------+-----------------------------+ //| 1 | Arya | Stark | 3000 | | //| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | //| 300 | Tyrion | Lannister | 5000 | | //| 4 | Faceless | Man | 0 | Needs a name. | //+-----+------------+-----------+--------+-----------------------------+ //| | | TOTAL | 10000 | | //+-----+------------+-----------+--------+-----------------------------+ //Table with Custom Alignment for 2 columns. //========================================================================== //========================================================================== // Vertical Alignment? //========================================================================== // horizontal alignment is fine... what about vertical? lets add a row with // a column having multiple lines; and then play with VAlign t.AppendRow(table.Row{13, "Winter\nIs\nComing", "Valar\nMorghulis", 0, "You\n know\n nothing,\n Jon\n Snow!"}) // first without any custom VAlign t.SetCaption("Table with a Multi-line Row.\n") fmt.Println(t.Render()) //+-----+------------+-----------+--------+-----------------------------+ //| # | FIRST NAME | LAST NAME | SALARY | | //+-----+------------+-----------+--------+-----------------------------+ //| 1 | Arya | Stark | 3000 | | //| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | //| 300 | Tyrion | Lannister | 5000 | | //| 4 | Faceless | Man | 0 | Needs a name. | //| 13 | Winter | Valar | 0 | You | //| | Is | Morghulis | | know | //| | Coming | | | nothing, | //| | | | | Jon | //| | | | | Snow! | //+-----+------------+-----------+--------+-----------------------------+ //| | | TOTAL | 10000 | | //+-----+------------+-----------+--------+-----------------------------+ //Table with a Multi-line Row. // // time to Align/VAlign the columns... t.SetColumnConfigs([]table.ColumnConfig{ {Name: "First Name", Align: text.AlignRight, VAlign: text.VAlignMiddle}, {Name: "Last Name", VAlign: text.VAlignBottom}, {Name: "Salary", Align: text.AlignRight, VAlign: text.VAlignMiddle}, // the 5th column does not have a title, so use the column number {Number: 5, Align: text.AlignJustify}, }) t.SetCaption("Table with a Multi-line Row with VAlign.\n") fmt.Println(t.Render()) //+-----+------------+-----------+--------+-----------------------------+ //| # | FIRST NAME | LAST NAME | SALARY | | //+-----+------------+-----------+--------+-----------------------------+ //| 1 | Arya | Stark | 3000 | | //| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | //| 300 | Tyrion | Lannister | 5000 | | //| 4 | Faceless | Man | 0 | Needs a name. | //| 13 | | | | You | //| | Winter | | | know | //| | Is | | 0 | nothing, | //| | Coming | Valar | | Jon | //| | | Morghulis | | Snow! | //+-----+------------+-----------+--------+-----------------------------+ //| | | TOTAL | 10000 | | //+-----+------------+-----------+--------+-----------------------------+ //Table with a Multi-line Row with VAlign. // // changed your mind about AlignJustify? t.SetColumnConfigs([]table.ColumnConfig{ {Name: "First Name", Align: text.AlignRight, VAlign: text.VAlignMiddle}, {Name: "Last Name", VAlign: text.VAlignBottom}, {Name: "Salary", Align: text.AlignRight, VAlign: text.VAlignMiddle}, {Number: 5, Align: text.AlignCenter}, }) t.SetCaption("Table with a Multi-line Row with VAlign and changed Align.\n") fmt.Println(t.Render()) //+-----+------------+-----------+--------+-----------------------------+ //| # | FIRST NAME | LAST NAME | SALARY | | //+-----+------------+-----------+--------+-----------------------------+ //| 1 | Arya | Stark | 3000 | | //| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | //| 300 | Tyrion | Lannister | 5000 | | //| 4 | Faceless | Man | 0 | Needs a name. | //| 13 | | | | You | //| | Winter | | | know | //| | Is | | 0 | nothing, | //| | Coming | Valar | | Jon | //| | | Morghulis | | Snow! | //+-----+------------+-----------+--------+-----------------------------+ //| | | TOTAL | 10000 | | //+-----+------------+-----------+--------+-----------------------------+ //Table with a Multi-line Row with VAlign and changed Align. //========================================================================== //========================================================================== // Time to begin anew. Too much on the screen for a demo! How about some // custom separators? //========================================================================== t.ResetRows() t.AppendRow(table.Row{1, "Arya", "Stark", 3000}) t.AppendRow(table.Row{20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"}) t.AppendSeparator() t.AppendRow([]interface{}{300, "Tyrion", "Lannister", 5000}) t.SetCaption("Simple Table with 3 Rows and a Separator in-between.\n") fmt.Println(t.Render()) //+-----+--------+-----------+------+-----------------------------+ //| 1 | Arya | Stark | 3000 | | //| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | //+-----+--------+-----------+------+-----------------------------+ //| 300 | Tyrion | Lannister | 5000 | | //+-----+--------+-----------+------+-----------------------------+ //Simple Table with 3 Rows and a Separator in-between. //========================================================================== //========================================================================== // Never-mind, lets start over yet again! //========================================================================== t.ResetRows() t.SetColumnConfigs(nil) t.AppendRow(table.Row{1, "Arya", "Stark", 3000}) t.AppendRow(table.Row{20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"}) t.AppendRow([]interface{}{300, "Tyrion", "Lannister", 5000}) t.SetCaption("Starting afresh with a Simple Table again.\n") fmt.Println(t.Render()) //+-----+------------+-----------+--------+-----------------------------+ //| # | FIRST NAME | LAST NAME | SALARY | | //+-----+------------+-----------+--------+-----------------------------+ //| 1 | Arya | Stark | 3000 | | //| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | //| 300 | Tyrion | Lannister | 5000 | | //+-----+------------+-----------+--------+-----------------------------+ //| | | TOTAL | 10000 | | //+-----+------------+-----------+--------+-----------------------------+ //Starting afresh with a Simple Table again. //========================================================================== //========================================================================== // Does it support paging? //========================================================================== t.SetPageSize(1) t.Style().Box.PageSeparator = "\n... page break ..." t.SetCaption("Table with a PageSize of 1.\n") fmt.Println(t.Render()) //+-----+------------+-----------+--------+-----------------------------+ //| # | FIRST NAME | LAST NAME | SALARY | | //+-----+------------+-----------+--------+-----------------------------+ //| 1 | Arya | Stark | 3000 | | //+-----+------------+-----------+--------+-----------------------------+ //| | | TOTAL | 10000 | | //+-----+------------+-----------+--------+-----------------------------+ //... page break ... //+-----+------------+-----------+--------+-----------------------------+ //| # | FIRST NAME | LAST NAME | SALARY | | //+-----+------------+-----------+--------+-----------------------------+ //| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | //+-----+------------+-----------+--------+-----------------------------+ //| | | TOTAL | 10000 | | //+-----+------------+-----------+--------+-----------------------------+ //... page break ... //+-----+------------+-----------+--------+-----------------------------+ //| # | FIRST NAME | LAST NAME | SALARY | | //+-----+------------+-----------+--------+-----------------------------+ //| 300 | Tyrion | Lannister | 5000 | | //+-----+------------+-----------+--------+-----------------------------+ //| | | TOTAL | 10000 | | //+-----+------------+-----------+--------+-----------------------------+ //Table with a PageSize of 1. t.SetPageSize(0) // disables paging //========================================================================== //========================================================================== // How about limiting the length of every Row? //========================================================================== t.SetAllowedRowLength(50) t.SetCaption("Table with an Allowed Row Length of 50.\n") fmt.Println(t.Render()) //+-----+------------+-----------+--------+------- ~ //| # | FIRST NAME | LAST NAME | SALARY | ~ //+-----+------------+-----------+--------+------- ~ //| 1 | Arya | Stark | 3000 | ~ //| 20 | Jon | Snow | 2000 | You kn ~ //| 300 | Tyrion | Lannister | 5000 | ~ //+-----+------------+-----------+--------+------- ~ //| | | TOTAL | 10000 | ~ //+-----+------------+-----------+--------+------- ~ t.SetStyle(table.StyleDouble) t.SetCaption("Table with an Allowed Row Length of 50 in 'StyleDouble'.\n") fmt.Println(t.Render()) //╔═════╦════════════╦═══════════╦════════╦═══════ ≈ //║ # ║ FIRST NAME ║ LAST NAME ║ SALARY ║ ≈ //╠═════╬════════════╬═══════════╬════════╬═══════ ≈ //║ 1 ║ Arya ║ Stark ║ 3000 ║ ≈ //║ 20 ║ Jon ║ Snow ║ 2000 ║ You kn ≈ //║ 300 ║ Tyrion ║ Lannister ║ 5000 ║ ≈ //╠═════╬════════════╬═══════════╬════════╬═══════ ≈ //║ ║ ║ TOTAL ║ 10000 ║ ≈ //╚═════╩════════════╩═══════════╩════════╩═══════ ≈ //Table with an Allowed Row Length of 50 in 'StyleDouble'. //========================================================================== //========================================================================== // But I want to see all the data! //========================================================================== t.SetColumnConfigs([]table.ColumnConfig{ {Name: "First Name", WidthMax: 6}, {Name: "Last Name", WidthMax: 9}, {Name: "Salary", WidthMax: 6}, {Number: 5, WidthMax: 10}, }) t.SetCaption("Table on a diet.\n") t.SetStyle(table.StyleRounded) fmt.Println(t.Render()) //╭─────┬────────┬───────────┬────────┬────────────╮ //│ # │ FIRST │ LAST NAME │ SALARY │ │ //│ │ NAME │ │ │ │ //├─────┼────────┼───────────┼────────┼────────────┤ //│ 1 │ Arya │ Stark │ 3000 │ │ //│ 20 │ Jon │ Snow │ 2000 │ You know n │ //│ │ │ │ │ othing, Jo │ //│ │ │ │ │ n Snow! │ //│ 300 │ Tyrion │ Lannister │ 5000 │ │ //├─────┼────────┼───────────┼────────┼────────────┤ //│ │ │ TOTAL │ 10000 │ │ //╰─────┴────────┴───────────┴────────┴────────────╯ //Table on a diet. t.SetAllowedRowLength(0) // remove the width restrictions t.SetColumnConfigs([]table.ColumnConfig{}) //========================================================================== //========================================================================== // ASCII is too simple for me. //========================================================================== t.SetStyle(table.StyleLight) t.SetCaption("Table using the style 'StyleLight'.\n") fmt.Println(t.Render()) //┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐ //│ # │ FIRST NAME │ LAST NAME │ SALARY │ │ //├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ //│ 1 │ Arya │ Stark │ 3000 │ │ //│ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ //│ 300 │ Tyrion │ Lannister │ 5000 │ │ //├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ //│ │ │ TOTAL │ 10000 │ │ //└─────┴────────────┴───────────┴────────┴─────────────────────────────┘ //Table using the style 'StyleLight'. t.SetStyle(table.StyleDouble) t.SetCaption("Table using the style '%s'.\n", t.Style().Name) fmt.Println(t.Render()) //╔═════╦════════════╦═══════════╦════════╦═════════════════════════════╗ //║ # ║ FIRST NAME ║ LAST NAME ║ SALARY ║ ║ //╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣ //║ 1 ║ Arya ║ Stark ║ 3000 ║ ║ //║ 20 ║ Jon ║ Snow ║ 2000 ║ You know nothing, Jon Snow! ║ //║ 300 ║ Tyrion ║ Lannister ║ 5000 ║ ║ //╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣ //║ ║ ║ TOTAL ║ 10000 ║ ║ //╚═════╩════════════╩═══════════╩════════╩═════════════════════════════╝ //Table using the style 'StyleDouble'. //========================================================================== //========================================================================== // I don't like any of the ready-made styles. //========================================================================== t.SetStyle(table.Style{ Name: "funkyStyle", Box: table.BoxStyle{ BottomLeft: "\\", BottomRight: "/", BottomSeparator: "v", Left: "[", LeftSeparator: "{", MiddleHorizontal: "-", MiddleSeparator: "+", MiddleVertical: "|", PaddingLeft: "<", PaddingRight: ">", Right: "]", RightSeparator: "}", TopLeft: "(", TopRight: ")", TopSeparator: "^", UnfinishedRow: " ~~~", }, }) t.Style().Format = table.FormatOptions{ Footer: text.FormatLower, Header: text.FormatLower, Row: text.FormatUpper, } t.Style().Options.DrawBorder = true t.Style().Options.SeparateColumns = true t.Style().Options.SeparateFooter = true t.Style().Options.SeparateHeader = true t.SetCaption("Table using the style 'funkyStyle'.\n") fmt.Println(t.Render()) //(-----^------------^-----------^--------^-----------------------------) //[< #>||||< >] //{-----+------------+-----------+--------+-----------------------------} //[< 1>|||< 3000>|< >] //[< 20>|||< 2000>|] //[<300>|||< 5000>|< >] //{-----+------------+-----------+--------+-----------------------------} //[< >|< >||< 10000>|< >] //\-----v------------v-----------v--------v-----------------------------/ //Table using the style 'funkyStyle'. //========================================================================== //========================================================================== // I need some color in my life! //========================================================================== t.SetStyle(table.StyleBold) colorBOnW := text.Colors{text.BgWhite, text.FgBlack} // set colors using Colors/ColorsHeader/ColorsFooter t.SetColumnConfigs([]table.ColumnConfig{ {Name: "#", Colors: text.Colors{text.FgYellow}, ColorsHeader: colorBOnW}, {Name: "First Name", Colors: text.Colors{text.FgHiRed}, ColorsHeader: colorBOnW}, {Name: "Last Name", Colors: text.Colors{text.FgHiRed}, ColorsHeader: colorBOnW, ColorsFooter: colorBOnW}, {Name: "Salary", Colors: text.Colors{text.FgGreen}, ColorsHeader: colorBOnW, ColorsFooter: colorBOnW}, {Number: 5, Colors: text.Colors{text.FgCyan}, ColorsHeader: colorBOnW}, }) t.SetCaption("Table with Colors.\n") fmt.Println(t.Render()) //┏━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ //┃ # ┃ FIRST NAME ┃ LAST NAME ┃ SALARY ┃ ┃ //┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ //┃ 1 ┃ Arya ┃ Stark ┃ 3000 ┃ ┃ //┃ 20 ┃ Jon ┃ Snow ┃ 2000 ┃ You know nothing, Jon Snow! ┃ //┃ 300 ┃ Tyrion ┃ Lannister ┃ 5000 ┃ ┃ //┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ //┃ ┃ ┃ TOTAL ┃ 10000 ┃ ┃ //┗━━━━━┻━━━━━━━━━━━━┻━━━━━━━━━━━┻━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ //Table with Colors. // // "Table with Colors"??? where? i don't see any! well, you have to trust me // on this... the colors show on a terminal that supports it. to prove it, // lets print the same table line-by-line using "%#v" to see the control // sequences ... t.SetCaption("Table with Colors in Raw Mode.\n") for _, line := range strings.Split(t.Render(), "\n") { if line != "" { fmt.Printf("%#v\n", line) } } fmt.Println() //"┏━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓" //"┃\x1b[47;30m # \x1b[0m┃\x1b[47;30m FIRST NAME \x1b[0m┃\x1b[47;30m LAST NAME \x1b[0m┃\x1b[47;30m SALARY \x1b[0m┃\x1b[47;30m \x1b[0m┃" //"┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫" //"┃\x1b[33m 1 \x1b[0m┃\x1b[91m Arya \x1b[0m┃\x1b[91m Stark \x1b[0m┃\x1b[32m 3000 \x1b[0m┃\x1b[36m \x1b[0m┃" //"┃\x1b[33m 20 \x1b[0m┃\x1b[91m Jon \x1b[0m┃\x1b[91m Snow \x1b[0m┃\x1b[32m 2000 \x1b[0m┃\x1b[36m You know nothing, Jon Snow! \x1b[0m┃" //"┃\x1b[33m 300 \x1b[0m┃\x1b[91m Tyrion \x1b[0m┃\x1b[91m Lannister \x1b[0m┃\x1b[32m 5000 \x1b[0m┃\x1b[36m \x1b[0m┃" //"┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫" //"┃ ┃ ┃\x1b[47;30m TOTAL \x1b[0m┃\x1b[47;30m 10000 \x1b[0m┃ ┃" //"┗━━━━━┻━━━━━━━━━━━━┻━━━━━━━━━━━┻━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛" //"Table with Colors in Raw Mode." //"" // disable colors and revert to previous version of the column configs t.SetColumnConfigs([]table.ColumnConfig{}) //========================================================================== //========================================================================== // How about not asking me to set colors in such a verbose way? And I don't // like wasting my terminal space with borders and separators. //========================================================================== t.SetStyle(table.StyleColoredBright) t.SetCaption("Table with style 'StyleColoredBright'.\n") fmt.Println(t.Render()) // # FIRST NAME LAST NAME SALARY // 1 Arya Stark 3000 // 20 Jon Snow 2000 You know nothing, Jon Snow! // 300 Tyrion Lannister 5000 // TOTAL 10000 //Table with style 'StyleColoredBright'. t.SetStyle(table.StyleBold) //========================================================================== //========================================================================== // I don't like borders! //========================================================================== t.Style().Options.DrawBorder = false t.SetCaption("Table without Borders.\n") fmt.Println(t.Render()) // # ┃ FIRST NAME ┃ LAST NAME ┃ SALARY ┃ //━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 1 ┃ Arya ┃ Stark ┃ 3000 ┃ // 20 ┃ Jon ┃ Snow ┃ 2000 ┃ You know nothing, Jon Snow! // 300 ┃ Tyrion ┃ Lannister ┃ 5000 ┃ //━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // ┃ ┃ TOTAL ┃ 10000 ┃ //Table without Borders. //========================================================================== //========================================================================== // I like walls and borders everywhere! //========================================================================== t.Style().Options.DrawBorder = true t.Style().Options.SeparateRows = true t.SetCaption("Table with Borders Everywhere!\n") t.SetTitle("Divide!") fmt.Println(t.Render()) //┏━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ //┃ # ┃ FIRST NAME ┃ LAST NAME ┃ SALARY ┃ ┃ //┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ //┃ 1 ┃ Arya ┃ Stark ┃ 3000 ┃ ┃ //┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ //┃ 20 ┃ Jon ┃ Snow ┃ 2000 ┃ You know nothing, Jon Snow! ┃ //┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ //┃ 300 ┃ Tyrion ┃ Lannister ┃ 5000 ┃ ┃ //┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ //┃ ┃ ┃ TOTAL ┃ 10000 ┃ ┃ //┗━━━━━┻━━━━━━━━━━━━┻━━━━━━━━━━━┻━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ //Table with Borders Everywhere! //========================================================================== //========================================================================== // There is strength in Unity. //========================================================================== t.Style().Options.DrawBorder = false t.Style().Options.SeparateColumns = false t.Style().Options.SeparateFooter = false t.Style().Options.SeparateHeader = false t.Style().Options.SeparateRows = false t.SetCaption("(c) No one!") t.SetTitle("Unite!") fmt.Println(t.Render()) fmt.Println() // # FIRST NAME LAST NAME SALARY // 1 Arya Stark 3000 // 20 Jon Snow 2000 You know nothing, Jon Snow! // 300 Tyrion Lannister 5000 // TOTAL 10000 //Table without Any Borders or Separators! //========================================================================== //========================================================================== // I want CSV. //========================================================================== for _, line := range strings.Split(t.RenderCSV(), "\n") { fmt.Printf("[CSV] %s\n", line) } fmt.Println() //[CSV] #,First Name,Last Name,Salary, //[CSV] 1,Arya,Stark,3000, //[CSV] 20,Jon,Snow,2000,"You know nothing\, Jon Snow!" //[CSV] 300,Tyrion,Lannister,5000, //[CSV] ,,Total,10000, //========================================================================== //========================================================================== // Nope. I want a HTML Table. //========================================================================== for _, line := range strings.Split(t.RenderHTML(), "\n") { fmt.Printf("[HTML] %s\n", line) } fmt.Println() //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML] //[HTML]
#First NameLast NameSalary 
1AryaStark3000 
20JonSnow2000You know nothing, Jon Snow!
300TyrionLannister5000 
  Total10000 
//========================================================================== //========================================================================== // Nope. I want a Markdown Table now. //========================================================================== for _, line := range strings.Split(t.RenderMarkdown(), "\n") { fmt.Printf("[Markdown] %s\n", line) } fmt.Println() //[Markdown] | # | First Name | Last Name | Salary | | //[Markdown] | ---:| --- | --- | ---:| --- | //[Markdown] | 1 | Arya | Stark | 3000 | | //[Markdown] | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | //[Markdown] | 300 | Tyrion | Lannister | 5000 | | //[Markdown] | | | Total | 10000 | | //========================================================================== //========================================================================== // That's it for today! New features will always find a place in this demo! //========================================================================== } func demoTableEmoji() { styles := []table.Style{ table.StyleDefault, table.StyleLight, table.StyleColoredBright, } for _, style := range styles { tw := table.NewWriter() tw.AppendHeader(table.Row{"Key", "Value"}) tw.AppendRows([]table.Row{ {"Emoji 1 🥰", 1000}, {"Emoji 2 ⚔️", 2000}, {"Emoji 3 🎁", 3000}, {"Emoji 4 ツ", 4000}, }) tw.AppendFooter(table.Row{"Total", 10000}) tw.SetAutoIndex(true) tw.SetStyle(style) fmt.Println(tw.Render()) fmt.Println() } } func main() { demoWhat := "features" if len(os.Args) > 1 { demoWhat = os.Args[1] } switch strings.ToLower(demoWhat) { case "colors": demoTableColors() case "emoji": demoTableEmoji() default: demoTableFeatures() } } go-pretty-6.2.4/cmd/profile-list/000077500000000000000000000000001407250454200166345ustar00rootroot00000000000000go-pretty-6.2.4/cmd/profile-list/profile.go000066400000000000000000000017161407250454200206300ustar00rootroot00000000000000package main import ( "fmt" "os" "strconv" "github.com/jedib0t/go-pretty/v6/list" "github.com/pkg/profile" ) var ( listItem1 = "Game Of Thrones" listItems2 = []interface{}{"Winter", "Is", "Coming"} listItems3 = []interface{}{"This", "Is", "Known"} profilers = []func(*profile.Profile){ profile.CPUProfile, profile.MemProfileRate(512), } ) func profileRender(profiler func(profile2 *profile.Profile), n int) { defer profile.Start(profiler, profile.ProfilePath("./")).Stop() for i := 0; i < n; i++ { lw := list.NewWriter() lw.AppendItem(listItem1) lw.Indent() lw.AppendItems(listItems2) lw.Indent() lw.AppendItems(listItems3) lw.Render() } } func main() { numRenders := 100000 if len(os.Args) > 1 { var err error numRenders, err = strconv.Atoi(os.Args[2]) if err != nil { fmt.Printf("Invalid Argument: '%s'\n", os.Args[2]) os.Exit(1) } } for _, profiler := range profilers { profileRender(profiler, numRenders) } } go-pretty-6.2.4/cmd/profile-progress/000077500000000000000000000000001407250454200175255ustar00rootroot00000000000000go-pretty-6.2.4/cmd/profile-progress/profile.go000066400000000000000000000030231407250454200215120ustar00rootroot00000000000000package main import ( "fmt" "io/ioutil" "os" "strconv" "time" "github.com/jedib0t/go-pretty/v6/progress" "github.com/pkg/profile" ) var ( tracker1 = progress.Tracker{Message: "Calculating Total # 1", Total: 1000, Units: progress.UnitsDefault} tracker2 = progress.Tracker{Message: "Downloading File # 2", Total: 1000, Units: progress.UnitsBytes} tracker3 = progress.Tracker{Message: "Transferring Amount # 3", Total: 1000, Units: progress.UnitsCurrencyDollar} profilers = []func(*profile.Profile){ profile.CPUProfile, profile.MemProfileRate(512), } ) func profileRender(profiler func(profile2 *profile.Profile), n int) { defer profile.Start(profiler, profile.ProfilePath("./")).Stop() trackSomething := func(pw progress.Writer, tracker *progress.Tracker) { tracker.Reset() pw.AppendTracker(tracker) time.Sleep(time.Millisecond * 100) tracker.Increment(tracker.Total / 2) time.Sleep(time.Millisecond * 100) tracker.Increment(tracker.Total / 2) } for i := 0; i < n; i++ { pw := progress.NewWriter() pw.SetAutoStop(true) pw.SetOutputWriter(ioutil.Discard) go trackSomething(pw, &tracker1) go trackSomething(pw, &tracker2) go trackSomething(pw, &tracker3) time.Sleep(time.Millisecond * 50) pw.Render() } } func main() { numRenders := 5 if len(os.Args) > 1 { var err error numRenders, err = strconv.Atoi(os.Args[2]) if err != nil { fmt.Printf("Invalid Argument: '%s'\n", os.Args[2]) os.Exit(1) } } for _, profiler := range profilers { profileRender(profiler, numRenders) } } go-pretty-6.2.4/cmd/profile-table/000077500000000000000000000000001407250454200167505ustar00rootroot00000000000000go-pretty-6.2.4/cmd/profile-table/profile.go000066400000000000000000000023241407250454200207400ustar00rootroot00000000000000package main import ( "fmt" "os" "strconv" "github.com/jedib0t/go-pretty/v6/table" "github.com/pkg/profile" ) var ( profilers = []func(*profile.Profile){ profile.CPUProfile, profile.MemProfileRate(512), } tableCaption = "Profiling a Simple Table." tableRowFooter = table.Row{"", "", "Total", 10000} tableRowHeader = table.Row{"#", "First Name", "Last Name", "Salary"} tableRows = []table.Row{ {1, "Arya", "Stark", 3000}, {20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"}, {300, "Tyrion", "Lannister", 5000}, } ) func profileRender(profiler func(profile2 *profile.Profile), n int) { defer profile.Start(profiler, profile.ProfilePath(".")).Stop() for i := 0; i < n; i++ { tw := table.NewWriter() tw.AppendHeader(tableRowHeader) tw.AppendRows(tableRows) tw.AppendFooter(tableRowFooter) tw.SetCaption(tableCaption) tw.Render() tw.RenderCSV() tw.RenderHTML() tw.RenderMarkdown() } } func main() { numRenders := 100000 if len(os.Args) > 1 { var err error numRenders, err = strconv.Atoi(os.Args[2]) if err != nil { fmt.Printf("Invalid Argument: '%s'\n", os.Args[2]) os.Exit(1) } } for _, profiler := range profilers { profileRender(profiler, numRenders) } } go-pretty-6.2.4/go.mod000066400000000000000000000005511407250454200145670ustar00rootroot00000000000000module github.com/jedib0t/go-pretty/v6 go 1.13 require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/fzipp/gocyclo v0.3.1 // indirect github.com/mattn/go-runewidth v0.0.9 github.com/pkg/profile v1.2.1 github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/testify v1.2.2 golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c ) go-pretty-6.2.4/go.sum000066400000000000000000000023231407250454200146130ustar00rootroot00000000000000github.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/fzipp/gocyclo v0.3.1 h1:A9UeX3HJSXTBzvHzhqoYVuE0eAhe+aM8XBCCwsPMZOc= github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= 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/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c h1:uHnKXcvx6SNkuwC+nrzxkJ+TpPwZOtumbhWrrOYN5YA= golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= go-pretty-6.2.4/list/000077500000000000000000000000001407250454200144335ustar00rootroot00000000000000go-pretty-6.2.4/list/README.md000066400000000000000000000015041407250454200157120ustar00rootroot00000000000000## List [![Go Reference](https://pkg.go.dev/badge/github.com/jedib0t/go-pretty/v6/list.svg)](https://pkg.go.dev/github.com/jedib0t/go-pretty/v6/list) Pretty-print lists with multiple levels/indents into ASCII/Unicode strings. - Append Items one-by-one or as a group - Indent/UnIndent as you like - Support Items with Multiple-lines - Mirror output to an io.Writer object (like os.StdOut) - Completely customizable styles - Many ready-to-use styles: [style.go](style.go) - Render as: - (ASCII/Unicode) List - HTML List (with custom CSS Class) - Markdown List ``` ■ Game Of Thrones ■ Winter ■ Is ■ Coming ■ This ■ Is ■ Known ■ The Dark Tower ■ The Gunslinger ``` A demonstration of all the capabilities can be found here: [../cmd/demo-list](../cmd/demo-list) go-pretty-6.2.4/list/list.go000066400000000000000000000110061407250454200157330ustar00rootroot00000000000000package list import ( "fmt" "io" "strings" "unicode/utf8" ) const ( // DefaultHTMLCSSClass stores the css-class to use when none-provided via // SetHTMLCSSClass(cssClass string). DefaultHTMLCSSClass = "go-pretty-table" ) // listItem represents one line in the List type listItem struct { Level int Text string } // List helps print a 2-dimensional array in a human readable pretty-List. type List struct { // approxSize stores the approximate output length/size approxSize int // htmlCSSClass stores the HTML CSS Class to use on the
    node htmlCSSClass string // items contains the list of items to render items []*listItem // level stores the current indentation level level int // outputMirror stores an io.Writer where the "Render" functions would write outputMirror io.Writer // style contains all the strings used to draw the List, and more style *Style } // AppendItem appends the item to the List of items to render. func (l *List) AppendItem(item interface{}) { l.items = append(l.items, l.analyzeAndStringify(item)) } // AppendItems appends the items to the List of items to render. func (l *List) AppendItems(items []interface{}) { for _, item := range items { l.AppendItem(item) } } // Indent indents the following items to appear right-shifted. func (l *List) Indent() { if len(l.items) == 0 { // should not indent when there is no item in the current level } else if l.level > l.items[len(l.items)-1].Level { // already indented compared to previous item; do not indent more } else { l.level++ } } // Length returns the number of items to be rendered. func (l *List) Length() int { return len(l.items) } // Reset sets the List to its initial state. func (l *List) Reset() { l.approxSize = 0 l.items = make([]*listItem, 0) l.level = 0 l.style = nil } // SetHTMLCSSClass sets the the HTML CSS Class to use on the
      node // when rendering the List in HTML format. Recursive lists would use a numbered // index suffix. For ex., if the cssClass is set as "foo"; the
        for level 0 // would have the class set as "foo"; the
          for level 1 would have "foo-1". func (l *List) SetHTMLCSSClass(cssClass string) { l.htmlCSSClass = cssClass } // SetOutputMirror sets an io.Writer for all the Render functions to "Write" to // in addition to returning a string. func (l *List) SetOutputMirror(mirror io.Writer) { l.outputMirror = mirror } // SetStyle overrides the DefaultStyle with the provided one. func (l *List) SetStyle(style Style) { l.style = &style } // Style returns the current style. func (l *List) Style() *Style { if l.style == nil { tempStyle := StyleDefault l.style = &tempStyle } return l.style } func (l *List) analyzeAndStringify(item interface{}) *listItem { itemStr := fmt.Sprint(item) if strings.Contains(itemStr, "\t") { itemStr = strings.Replace(itemStr, "\t", " ", -1) } if strings.Contains(itemStr, "\r") { itemStr = strings.Replace(itemStr, "\r", "", -1) } return &listItem{ Level: l.level, Text: itemStr, } } // UnIndent un-indents the following items to appear left-shifted. func (l *List) UnIndent() { if l.level > 0 { l.level-- } } func (l *List) initForRender() { // pick a default style l.Style() // calculate the approximate size needed by looking at all entries l.approxSize = 0 for _, item := range l.items { // account for the following when incrementing approxSize: // 1. prefix, 2. padding, 3. bullet, 4. text, 5. newline l.approxSize += utf8.RuneCountInString(l.style.LinePrefix) if item.Level > 0 { l.approxSize += utf8.RuneCountInString(l.style.CharItemVertical) * item.Level } l.approxSize += utf8.RuneCountInString(l.style.CharItemVertical) l.approxSize += utf8.RuneCountInString(item.Text) l.approxSize += utf8.RuneCountInString(l.style.CharNewline) } // default to a HTML CSS Class if none-defined if l.htmlCSSClass == "" { l.htmlCSSClass = DefaultHTMLCSSClass } } func (l *List) hasMoreItemsInLevel(levelIdx int, fromItemIdx int) bool { for idx := fromItemIdx + 1; idx >= 0 && idx < len(l.items); idx++ { if l.items[idx].Level < levelIdx { return false } else if l.items[idx].Level == levelIdx { return true } } return false } func (l *List) render(out *strings.Builder) string { outStr := out.String() if l.outputMirror != nil && len(outStr) > 0 { l.outputMirror.Write([]byte(outStr)) l.outputMirror.Write([]byte("\n")) } return outStr } // renderHint has hints for the Render*() logic type renderHint struct { isTopItem bool isFirstItem bool isOnlyItem bool isLastItem bool isBottomItem bool } go-pretty-6.2.4/list/list_test.go000066400000000000000000000070231407250454200167760ustar00rootroot00000000000000package list import ( "testing" "github.com/stretchr/testify/assert" ) var ( testCSSClass = "test-css-class" testItem1 = "Game Of Thrones" testItem1ML = testItem1 + "\n\t// George. R. R. Martin" testItems2 = []interface{}{"Winter", "Is", "Coming"} testItems2ML = []interface{}{"Winter\r\nIs\nComing", "Is", "Coming"} testItems3 = []interface{}{"This", "Is", "Known"} testItems3ML = []interface{}{"This\nIs\nKnown", "Is", "Known"} testItem4 = "The Dark Tower" testItem4ML = testItem4 + "\n\t// Stephen King" testItem5 = "The Gunslinger" ) type myMockOutputMirror struct { mirroredOutput string } func (t *myMockOutputMirror) Write(p []byte) (n int, err error) { t.mirroredOutput += string(p) return len(p), nil } func TestNewWriter(t *testing.T) { lw := NewWriter() assert.NotNil(t, lw.Style()) assert.Equal(t, StyleDefault, *lw.Style()) lw.SetStyle(StyleConnectedBold) assert.NotNil(t, lw.Style()) assert.Equal(t, StyleConnectedBold, *lw.Style()) } func TestList_AppendItem(t *testing.T) { list := List{} assert.Equal(t, 0, list.Length()) list.AppendItem(testItem1) list.AppendItem(testItem1) assert.Equal(t, 2, list.Length()) } func TestList_AppendItems(t *testing.T) { list := List{} assert.Equal(t, 0, list.Length()) list.AppendItems(testItems2) assert.Equal(t, len(testItems2), list.Length()) } func TestList_Indent(t *testing.T) { list := List{} assert.Equal(t, 0, list.level) // should not indent when there is no item in the list list.Indent() assert.Equal(t, 0, list.level) // should indent with an item in the list list.AppendItem(testItem1) list.Indent() assert.Equal(t, 1, list.level) // should not indent if the previous item will then become "2 levels below" list.Indent() assert.Equal(t, 1, list.level) } func TestList_Length(t *testing.T) { list := List{} assert.Equal(t, 0, list.Length()) list.AppendItem(testItem1) assert.Equal(t, 1, list.Length()) } func TestList_Reset(t *testing.T) { list := List{} list.SetStyle(StyleBulletCircle) assert.Equal(t, "", list.Render()) list.AppendItem(testItem1) assert.Equal(t, "● Game Of Thrones", list.Render()) list.Reset() assert.Equal(t, "", list.Render()) } func TestList_SetHTMLCSSClass(t *testing.T) { list := List{} assert.Empty(t, list.htmlCSSClass) list.SetHTMLCSSClass(testCSSClass) assert.Equal(t, testCSSClass, list.htmlCSSClass) } func TestList_SetOutputMirror(t *testing.T) { list := List{} list.AppendItem(testItem1) expectedOut := "* Game Of Thrones" assert.Equal(t, nil, list.outputMirror) assert.Equal(t, expectedOut, list.Render()) mockOutputMirror := &myMockOutputMirror{} list.SetOutputMirror(mockOutputMirror) assert.Equal(t, mockOutputMirror, list.outputMirror) assert.Equal(t, expectedOut, list.Render()) assert.Equal(t, expectedOut+"\n", mockOutputMirror.mirroredOutput) } func TestList_SetStyle(t *testing.T) { list := List{} assert.NotNil(t, list.Style()) list.AppendItem(testItem1) list.Indent() list.AppendItems(testItems2) expectedOut := `* Game Of Thrones * Winter * Is * Coming` assert.Equal(t, expectedOut, list.Render()) list.SetStyle(StyleConnectedLight) assert.NotNil(t, list.Style()) assert.Equal(t, &StyleConnectedLight, list.Style()) expectedOut = `── Game Of Thrones ├─ Winter ├─ Is └─ Coming` assert.Equal(t, expectedOut, list.Render()) } func TestList_UnIndent(t *testing.T) { list := List{level: 2} list.UnIndent() assert.Equal(t, 1, list.level) list.UnIndent() assert.Equal(t, 0, list.level) list.UnIndent() assert.Equal(t, 0, list.level) } go-pretty-6.2.4/list/render.go000066400000000000000000000060461407250454200162470ustar00rootroot00000000000000package list import ( "strings" "unicode/utf8" ) // Render renders the List in a human-readable "pretty" format. Example: // * Game Of Thrones // * Winter // * Is // * Coming // * This // * Is // * Known // * The Dark Tower // * The Gunslinger func (l *List) Render() string { l.initForRender() var out strings.Builder out.Grow(l.approxSize) for idx, item := range l.items { hint := renderHint{ isTopItem: bool(idx == 0), isFirstItem: bool(idx == 0 || item.Level > l.items[idx-1].Level), isLastItem: !l.hasMoreItemsInLevel(item.Level, idx), isBottomItem: bool(idx == len(l.items)-1), } if hint.isFirstItem && hint.isLastItem { hint.isOnlyItem = true } l.renderItem(&out, idx, item, hint) } return l.render(&out) } func (l *List) renderItem(out *strings.Builder, idx int, item *listItem, hint renderHint) { // when working on item number 2 or more, render a newline first if idx > 0 { out.WriteRune('\n') } // format item.Text as directed in l.style itemStr := l.style.Format.Apply(item.Text) // convert newlines if newlines are not "\n" in l.style if strings.Contains(itemStr, "\n") && l.style.CharNewline != "\n" { itemStr = strings.Replace(itemStr, "\n", l.style.CharNewline, -1) } // render the item.Text line by line for lineIdx, lineStr := range strings.Split(itemStr, "\n") { if lineIdx > 0 { out.WriteRune('\n') } // render the prefix or the leading text before the actual item l.renderItemBulletPrefix(out, idx, item.Level, lineIdx, hint) l.renderItemBullet(out, idx, item.Level, lineIdx, hint) // render the actual item out.WriteString(lineStr) } } func (l *List) renderItemBullet(out *strings.Builder, itemIdx int, itemLevel int, lineIdx int, hint renderHint) { if lineIdx > 0 { // multi-line item.Text if hint.isLastItem { out.WriteString(strings.Repeat(" ", utf8.RuneCountInString(l.style.CharItemVertical))) } else { out.WriteString(l.style.CharItemVertical) } } else { // single-line item.Text (or first line of a multi-line item.Text) if hint.isOnlyItem { if hint.isTopItem { out.WriteString(l.style.CharItemSingle) } else { out.WriteString(l.style.CharItemBottom) } } else if hint.isTopItem { out.WriteString(l.style.CharItemTop) } else if hint.isFirstItem { out.WriteString(l.style.CharItemFirst) } else if hint.isBottomItem || hint.isLastItem { out.WriteString(l.style.CharItemBottom) } else { out.WriteString(l.style.CharItemMiddle) } out.WriteRune(' ') } } func (l *List) renderItemBulletPrefix(out *strings.Builder, itemIdx int, itemLevel int, lineIdx int, hint renderHint) { // write a prefix if one has been set in l.style if l.style.LinePrefix != "" { out.WriteString(l.style.LinePrefix) } // render spaces and connectors until the item's position for levelIdx := 0; levelIdx < itemLevel; levelIdx++ { if l.hasMoreItemsInLevel(levelIdx, itemIdx) { out.WriteString(l.style.CharItemVertical) } else { out.WriteString(strings.Repeat(" ", utf8.RuneCountInString(l.style.CharItemVertical))) } } } go-pretty-6.2.4/list/render_html.go000066400000000000000000000034531407250454200172720ustar00rootroot00000000000000package list import ( "html" "strconv" "strings" ) // RenderHTML renders the List in the HTML format. Example: //
            //
          • Game Of Thrones
          • //
              //
            • Winter
            • //
            • Is
            • //
            • Coming
            • //
                //
              • This
              • //
              • Is
              • //
              • Known
              • //
              //
            //
          • The Dark Tower
          • //
              //
            • The Gunslinger
            • //
            //
          func (l *List) RenderHTML() string { l.initForRender() var out strings.Builder if len(l.items) > 0 { l.htmlRenderRecursively(&out, 0, l.items[0]) } return l.render(&out) } func (l *List) htmlRenderRecursively(out *strings.Builder, idx int, item *listItem) int { linePrefix := strings.Repeat(" ", item.Level) out.WriteString(linePrefix) out.WriteString("
            0 { out.WriteRune('-') out.WriteString(strconv.Itoa(item.Level)) } out.WriteString("\">\n") var numItemsRendered int for itemIdx := idx; itemIdx < len(l.items); itemIdx++ { if l.items[itemIdx].Level == item.Level { out.WriteString(linePrefix) out.WriteString("
          • ") out.WriteString(strings.Replace(html.EscapeString(l.items[itemIdx].Text), "\n", "
            ", -1)) out.WriteString("
          • \n") numItemsRendered++ } else if l.items[itemIdx].Level > item.Level { // indent numItemsRenderedRecursively := l.htmlRenderRecursively(out, itemIdx, l.items[itemIdx]) numItemsRendered += numItemsRenderedRecursively itemIdx += numItemsRenderedRecursively - 1 if numItemsRendered > 0 { out.WriteRune('\n') } } else { // un-indent break } } out.WriteString(linePrefix) out.WriteString("
          ") return numItemsRendered } go-pretty-6.2.4/list/render_html_test.go000066400000000000000000000052461407250454200203330ustar00rootroot00000000000000package list import ( "testing" "github.com/stretchr/testify/assert" ) func TestList_RenderHTML(t *testing.T) { lw := NewWriter() lw.AppendItem(testItem1) lw.Indent() lw.AppendItems(testItems2) lw.Indent() lw.AppendItems(testItems3) lw.UnIndent() lw.UnIndent() lw.AppendItem(testItem4) lw.Indent() lw.AppendItem(testItem5) lw.SetHTMLCSSClass(testCSSClass) expectedOut := `
          • Game Of Thrones
            • Winter
            • Is
            • Coming
              • This
              • Is
              • Known
          • The Dark Tower
            • The Gunslinger
          ` assert.Equal(t, expectedOut, lw.RenderHTML()) } func TestList_RenderHTML_Complex(t *testing.T) { lw := NewWriter() lw.AppendItem("The Houses of Westeros") lw.Indent() lw.AppendItem("The Starks of Winterfell") lw.Indent() lw.AppendItem("Eddard Stark") lw.Indent() lw.AppendItems([]interface{}{"Robb Stark", "Sansa Stark", "Arya Stark", "Bran Stark", "Rickon Stark"}) lw.UnIndent() lw.AppendItems([]interface{}{"Lyanna Stark", "Benjen Stark"}) lw.UnIndent() lw.AppendItem("The Targaryens of Dragonstone") lw.Indent() lw.AppendItem("Aerys Targaryen") lw.Indent() lw.AppendItems([]interface{}{"Rhaegar Targaryen", "Viserys Targaryen", "Daenerys Targaryen"}) lw.UnIndent() lw.UnIndent() lw.AppendItem("The Lannisters of Lannisport") lw.Indent() lw.AppendItem("Tywin Lannister") lw.Indent() lw.AppendItems([]interface{}{"Cersei Lannister", "Jaime Lannister", "Tyrion Lannister"}) expectedOut := `
          • The Houses of Westeros
            • The Starks of Winterfell
              • Eddard Stark
                • Robb Stark
                • Sansa Stark
                • Arya Stark
                • Bran Stark
                • Rickon Stark
              • Lyanna Stark
              • Benjen Stark
            • The Targaryens of Dragonstone
              • Aerys Targaryen
                • Rhaegar Targaryen
                • Viserys Targaryen
                • Daenerys Targaryen
            • The Lannisters of Lannisport
              • Tywin Lannister
                • Cersei Lannister
                • Jaime Lannister
                • Tyrion Lannister
          ` assert.Equal(t, expectedOut, lw.RenderHTML()) } go-pretty-6.2.4/list/render_markdown.go000066400000000000000000000011751407250454200201470ustar00rootroot00000000000000package list // RenderMarkdown renders the List in the Markdown format. Example: // * Game Of Thrones // * Winter // * Is // * Coming // * This // * Is // * Known // * The Dark Tower // * The Gunslinger func (l *List) RenderMarkdown() string { // make a copy of the original style and ensure it is restored on exit originalStyle := l.style defer func() { if originalStyle == nil { l.style = nil } else { l.SetStyle(*originalStyle) } }() // override whatever style was set with StyleMarkdown l.SetStyle(StyleMarkdown) // render like a regular list return l.Render() } go-pretty-6.2.4/list/render_markdown_test.go000066400000000000000000000014351407250454200212050ustar00rootroot00000000000000package list import ( "testing" "github.com/stretchr/testify/assert" ) func TestList_RenderMarkdown(t *testing.T) { lw := NewWriter() lw.AppendItem(testItem1) lw.Indent() lw.AppendItems(testItems2) lw.Indent() lw.AppendItems(testItems3) lw.UnIndent() lw.AppendItem(testItem4) lw.Indent() lw.AppendItem(testItem5) expectedOutMarkdown := ` * Game Of Thrones * Winter * Is * Coming * This * Is * Known * The Dark Tower * The Gunslinger` assert.Equal(t, expectedOutMarkdown, lw.RenderMarkdown()) lw.SetStyle(styleTest) assert.NotNil(t, lw.Style()) assert.Equal(t, styleTest.Name, lw.Style().Name) assert.Equal(t, expectedOutMarkdown, lw.RenderMarkdown()) assert.NotNil(t, lw.Style()) assert.Equal(t, styleTest.Name, lw.Style().Name) } go-pretty-6.2.4/list/render_test.go000066400000000000000000000364741407250454200173160ustar00rootroot00000000000000package list import ( "fmt" "sort" "strings" "testing" "github.com/stretchr/testify/assert" ) func TestList_Render(t *testing.T) { lw := NewWriter() lw.AppendItem(testItem1) lw.Indent() lw.AppendItems(testItems2) lw.Indent() lw.AppendItems(testItems3) lw.UnIndent() lw.UnIndent() lw.AppendItem(testItem4) lw.Indent() lw.AppendItem(testItem5) lw.SetStyle(styleTest) expectedOut := `t Game Of Thrones |f Winter |m Is |b Coming | f This | m Is | b Known b The Dark Tower b The Gunslinger` assert.Equal(t, expectedOut, lw.Render()) } func TestList_Render_Complex(t *testing.T) { lw := NewWriter() lw.AppendItem("The Houses of Westeros") lw.Indent() lw.AppendItem("The Starks of Winterfell") lw.Indent() lw.AppendItem("Eddard Stark") lw.Indent() lw.AppendItems([]interface{}{"Robb Stark", "Sansa Stark", "Arya Stark", "Bran Stark", "Rickon Stark"}) lw.UnIndent() lw.AppendItems([]interface{}{"Lyanna Stark", "Benjen Stark"}) lw.UnIndent() lw.AppendItem("The Targaryens of Dragonstone") lw.Indent() lw.AppendItem("Aerys Targaryen") lw.Indent() lw.AppendItems([]interface{}{"Rhaegar Targaryen", "Viserys Targaryen", "Daenerys Targaryen"}) lw.UnIndent() lw.UnIndent() lw.AppendItem("The Lannisters of Lannisport") lw.Indent() lw.AppendItem("Tywin Lannister") lw.Indent() lw.AppendItems([]interface{}{"Cersei Lannister", "Jaime Lannister", "Tyrion Lannister"}) styles := map[Style]string{ StyleBulletCircle: "● The Houses of Westeros\n ● The Starks of Winterfell\n ● Eddard Stark\n ● Robb Stark\n ● Sansa Stark\n ● Arya Stark\n ● Bran Stark\n ● Rickon Stark\n ● Lyanna Stark\n ● Benjen Stark\n ● The Targaryens of Dragonstone\n ● Aerys Targaryen\n ● Rhaegar Targaryen\n ● Viserys Targaryen\n ● Daenerys Targaryen\n ● The Lannisters of Lannisport\n ● Tywin Lannister\n ● Cersei Lannister\n ● Jaime Lannister\n ● Tyrion Lannister", StyleBulletFlower: "✽ The Houses of Westeros\n ✽ The Starks of Winterfell\n ✽ Eddard Stark\n ✽ Robb Stark\n ✽ Sansa Stark\n ✽ Arya Stark\n ✽ Bran Stark\n ✽ Rickon Stark\n ✽ Lyanna Stark\n ✽ Benjen Stark\n ✽ The Targaryens of Dragonstone\n ✽ Aerys Targaryen\n ✽ Rhaegar Targaryen\n ✽ Viserys Targaryen\n ✽ Daenerys Targaryen\n ✽ The Lannisters of Lannisport\n ✽ Tywin Lannister\n ✽ Cersei Lannister\n ✽ Jaime Lannister\n ✽ Tyrion Lannister", StyleBulletSquare: "■ The Houses of Westeros\n ■ The Starks of Winterfell\n ■ Eddard Stark\n ■ Robb Stark\n ■ Sansa Stark\n ■ Arya Stark\n ■ Bran Stark\n ■ Rickon Stark\n ■ Lyanna Stark\n ■ Benjen Stark\n ■ The Targaryens of Dragonstone\n ■ Aerys Targaryen\n ■ Rhaegar Targaryen\n ■ Viserys Targaryen\n ■ Daenerys Targaryen\n ■ The Lannisters of Lannisport\n ■ Tywin Lannister\n ■ Cersei Lannister\n ■ Jaime Lannister\n ■ Tyrion Lannister", StyleBulletStar: "★ The Houses of Westeros\n ★ The Starks of Winterfell\n ★ Eddard Stark\n ★ Robb Stark\n ★ Sansa Stark\n ★ Arya Stark\n ★ Bran Stark\n ★ Rickon Stark\n ★ Lyanna Stark\n ★ Benjen Stark\n ★ The Targaryens of Dragonstone\n ★ Aerys Targaryen\n ★ Rhaegar Targaryen\n ★ Viserys Targaryen\n ★ Daenerys Targaryen\n ★ The Lannisters of Lannisport\n ★ Tywin Lannister\n ★ Cersei Lannister\n ★ Jaime Lannister\n ★ Tyrion Lannister", StyleBulletTriangle: "▶ The Houses of Westeros\n ▶ The Starks of Winterfell\n ▶ Eddard Stark\n ▶ Robb Stark\n ▶ Sansa Stark\n ▶ Arya Stark\n ▶ Bran Stark\n ▶ Rickon Stark\n ▶ Lyanna Stark\n ▶ Benjen Stark\n ▶ The Targaryens of Dragonstone\n ▶ Aerys Targaryen\n ▶ Rhaegar Targaryen\n ▶ Viserys Targaryen\n ▶ Daenerys Targaryen\n ▶ The Lannisters of Lannisport\n ▶ Tywin Lannister\n ▶ Cersei Lannister\n ▶ Jaime Lannister\n ▶ Tyrion Lannister", StyleConnectedBold: "━━ The Houses of Westeros\n ┣━ The Starks of Winterfell\n ┃ ┣━ Eddard Stark\n ┃ ┃ ┣━ Robb Stark\n ┃ ┃ ┣━ Sansa Stark\n ┃ ┃ ┣━ Arya Stark\n ┃ ┃ ┣━ Bran Stark\n ┃ ┃ ┗━ Rickon Stark\n ┃ ┣━ Lyanna Stark\n ┃ ┗━ Benjen Stark\n ┣━ The Targaryens of Dragonstone\n ┃ ┗━ Aerys Targaryen\n ┃ ┣━ Rhaegar Targaryen\n ┃ ┣━ Viserys Targaryen\n ┃ ┗━ Daenerys Targaryen\n ┗━ The Lannisters of Lannisport\n ┗━ Tywin Lannister\n ┣━ Cersei Lannister\n ┣━ Jaime Lannister\n ┗━ Tyrion Lannister", StyleConnectedDouble: "══ The Houses of Westeros\n ╠═ The Starks of Winterfell\n ║ ╠═ Eddard Stark\n ║ ║ ╠═ Robb Stark\n ║ ║ ╠═ Sansa Stark\n ║ ║ ╠═ Arya Stark\n ║ ║ ╠═ Bran Stark\n ║ ║ ╚═ Rickon Stark\n ║ ╠═ Lyanna Stark\n ║ ╚═ Benjen Stark\n ╠═ The Targaryens of Dragonstone\n ║ ╚═ Aerys Targaryen\n ║ ╠═ Rhaegar Targaryen\n ║ ╠═ Viserys Targaryen\n ║ ╚═ Daenerys Targaryen\n ╚═ The Lannisters of Lannisport\n ╚═ Tywin Lannister\n ╠═ Cersei Lannister\n ╠═ Jaime Lannister\n ╚═ Tyrion Lannister", StyleConnectedLight: "── The Houses of Westeros\n ├─ The Starks of Winterfell\n │ ├─ Eddard Stark\n │ │ ├─ Robb Stark\n │ │ ├─ Sansa Stark\n │ │ ├─ Arya Stark\n │ │ ├─ Bran Stark\n │ │ └─ Rickon Stark\n │ ├─ Lyanna Stark\n │ └─ Benjen Stark\n ├─ The Targaryens of Dragonstone\n │ └─ Aerys Targaryen\n │ ├─ Rhaegar Targaryen\n │ ├─ Viserys Targaryen\n │ └─ Daenerys Targaryen\n └─ The Lannisters of Lannisport\n └─ Tywin Lannister\n ├─ Cersei Lannister\n ├─ Jaime Lannister\n └─ Tyrion Lannister", StyleConnectedRounded: "── The Houses of Westeros\n ├─ The Starks of Winterfell\n │ ├─ Eddard Stark\n │ │ ├─ Robb Stark\n │ │ ├─ Sansa Stark\n │ │ ├─ Arya Stark\n │ │ ├─ Bran Stark\n │ │ ╰─ Rickon Stark\n │ ├─ Lyanna Stark\n │ ╰─ Benjen Stark\n ├─ The Targaryens of Dragonstone\n │ ╰─ Aerys Targaryen\n │ ├─ Rhaegar Targaryen\n │ ├─ Viserys Targaryen\n │ ╰─ Daenerys Targaryen\n ╰─ The Lannisters of Lannisport\n ╰─ Tywin Lannister\n ├─ Cersei Lannister\n ├─ Jaime Lannister\n ╰─ Tyrion Lannister", StyleDefault: "* The Houses of Westeros\n * The Starks of Winterfell\n * Eddard Stark\n * Robb Stark\n * Sansa Stark\n * Arya Stark\n * Bran Stark\n * Rickon Stark\n * Lyanna Stark\n * Benjen Stark\n * The Targaryens of Dragonstone\n * Aerys Targaryen\n * Rhaegar Targaryen\n * Viserys Targaryen\n * Daenerys Targaryen\n * The Lannisters of Lannisport\n * Tywin Lannister\n * Cersei Lannister\n * Jaime Lannister\n * Tyrion Lannister", StyleMarkdown: " * The Houses of Westeros\n * The Starks of Winterfell\n * Eddard Stark\n * Robb Stark\n * Sansa Stark\n * Arya Stark\n * Bran Stark\n * Rickon Stark\n * Lyanna Stark\n * Benjen Stark\n * The Targaryens of Dragonstone\n * Aerys Targaryen\n * Rhaegar Targaryen\n * Viserys Targaryen\n * Daenerys Targaryen\n * The Lannisters of Lannisport\n * Tywin Lannister\n * Cersei Lannister\n * Jaime Lannister\n * Tyrion Lannister", styleTest: "s The Houses of Westeros\n f The Starks of Winterfell\n |f Eddard Stark\n ||f Robb Stark\n ||m Sansa Stark\n ||m Arya Stark\n ||m Bran Stark\n ||b Rickon Stark\n |m Lyanna Stark\n |b Benjen Stark\n m The Targaryens of Dragonstone\n |b Aerys Targaryen\n | f Rhaegar Targaryen\n | m Viserys Targaryen\n | b Daenerys Targaryen\n b The Lannisters of Lannisport\n b Tywin Lannister\n f Cersei Lannister\n m Jaime Lannister\n b Tyrion Lannister", } var mismatches []string for style, expectedOut := range styles { lw.SetStyle(style) out := lw.Render() assert.Equal(t, expectedOut, out) if expectedOut != out { mismatches = append(mismatches, fmt.Sprintf("%s: %#v,", style.Name, out)) fmt.Printf("// %s renders a List like below:\n", style.Name) for _, line := range strings.Split(out, "\n") { fmt.Printf("// %s\n", line) } fmt.Println() } } sort.Strings(mismatches) for _, mismatch := range mismatches { fmt.Println(mismatch) } } func TestList_Render_Connected(t *testing.T) { lw := NewWriter() lw.SetStyle(StyleConnectedLight) assert.Empty(t, lw.Render()) lw.AppendItem(testItem1) expectedOut := "── Game Of Thrones" assert.Equal(t, expectedOut, lw.Render()) lw.AppendItem(testItem1) expectedOut = `┌─ Game Of Thrones └─ Game Of Thrones` assert.Equal(t, expectedOut, lw.Render()) lw.AppendItem(testItem1) expectedOut = `┌─ Game Of Thrones ├─ Game Of Thrones └─ Game Of Thrones` assert.Equal(t, expectedOut, lw.Render()) lw.Indent() lw.AppendItem(testItem1) expectedOut = `┌─ Game Of Thrones ├─ Game Of Thrones └─ Game Of Thrones └─ Game Of Thrones` assert.Equal(t, expectedOut, lw.Render()) lw.AppendItem(testItem1) expectedOut = `┌─ Game Of Thrones ├─ Game Of Thrones └─ Game Of Thrones ├─ Game Of Thrones └─ Game Of Thrones` assert.Equal(t, expectedOut, lw.Render()) lw.Indent() lw.AppendItem(testItem1) expectedOut = `┌─ Game Of Thrones ├─ Game Of Thrones └─ Game Of Thrones ├─ Game Of Thrones └─ Game Of Thrones └─ Game Of Thrones` assert.Equal(t, expectedOut, lw.Render()) lw.UnIndent() lw.AppendItem(testItem1) expectedOut = `┌─ Game Of Thrones ├─ Game Of Thrones └─ Game Of Thrones ├─ Game Of Thrones ├─ Game Of Thrones │ └─ Game Of Thrones └─ Game Of Thrones` assert.Equal(t, expectedOut, lw.Render()) lw.UnIndent() lw.AppendItem(testItem1) expectedOut = `┌─ Game Of Thrones ├─ Game Of Thrones ├─ Game Of Thrones │ ├─ Game Of Thrones │ ├─ Game Of Thrones │ │ └─ Game Of Thrones │ └─ Game Of Thrones └─ Game Of Thrones` assert.Equal(t, expectedOut, lw.Render()) } func TestList_Render_MultiLine(t *testing.T) { lw := NewWriter() lw.AppendItem(testItem1ML) lw.Indent() lw.AppendItems(testItems2ML) lw.Indent() lw.AppendItems(testItems3ML) lw.UnIndent() lw.UnIndent() lw.AppendItem(testItem4ML) lw.Indent() lw.AppendItem(testItem5) expectedOut := `* Game Of Thrones // George. R. R. Martin * Winter Is Coming * Is * Coming * This Is Known * Is * Known * The Dark Tower // Stephen King * The Gunslinger` assert.Equal(t, expectedOut, lw.Render()) expectedOutRounded := `╭─ Game Of Thrones │ // George. R. R. Martin │ ├─ Winter │ │ Is │ │ Coming │ ├─ Is │ ╰─ Coming │ ├─ This │ │ Is │ │ Known │ ├─ Is │ ╰─ Known ╰─ The Dark Tower // Stephen King ╰─ The Gunslinger` lw.SetStyle(StyleConnectedRounded) assert.Equal(t, expectedOutRounded, lw.Render()) expectedOutHTML := `
          • Game Of Thrones
            // George. R. R. Martin
            • Winter
              Is
              Coming
            • Is
            • Coming
              • This
                Is
                Known
              • Is
              • Known
          • The Dark Tower
            // Stephen King
            • The Gunslinger
          ` assert.Equal(t, expectedOutHTML, lw.RenderHTML()) expectedOutMarkdown := ` * Game Of Thrones
          // George. R. R. Martin * Winter
          Is
          Coming * Is * Coming * This
          Is
          Known * Is * Known * The Dark Tower
          // Stephen King * The Gunslinger` assert.Equal(t, expectedOutMarkdown, lw.RenderMarkdown()) } func TestList_Render_Styles(t *testing.T) { lw := NewWriter() lw.AppendItem(testItem1) lw.Indent() lw.AppendItems(testItems2) lw.Indent() lw.AppendItems(testItems3) lw.UnIndent() lw.UnIndent() lw.AppendItem(testItem4) lw.Indent() lw.AppendItem(testItem5) styles := map[Style]string{ StyleBulletCircle: "● Game Of Thrones\n ● Winter\n ● Is\n ● Coming\n ● This\n ● Is\n ● Known\n● The Dark Tower\n ● The Gunslinger", StyleBulletFlower: "✽ Game Of Thrones\n ✽ Winter\n ✽ Is\n ✽ Coming\n ✽ This\n ✽ Is\n ✽ Known\n✽ The Dark Tower\n ✽ The Gunslinger", StyleBulletSquare: "■ Game Of Thrones\n ■ Winter\n ■ Is\n ■ Coming\n ■ This\n ■ Is\n ■ Known\n■ The Dark Tower\n ■ The Gunslinger", StyleBulletStar: "★ Game Of Thrones\n ★ Winter\n ★ Is\n ★ Coming\n ★ This\n ★ Is\n ★ Known\n★ The Dark Tower\n ★ The Gunslinger", StyleBulletTriangle: "▶ Game Of Thrones\n ▶ Winter\n ▶ Is\n ▶ Coming\n ▶ This\n ▶ Is\n ▶ Known\n▶ The Dark Tower\n ▶ The Gunslinger", StyleConnectedBold: "┏━ Game Of Thrones\n┃ ┣━ Winter\n┃ ┣━ Is\n┃ ┗━ Coming\n┃ ┣━ This\n┃ ┣━ Is\n┃ ┗━ Known\n┗━ The Dark Tower\n ┗━ The Gunslinger", StyleConnectedDouble: "╔═ Game Of Thrones\n║ ╠═ Winter\n║ ╠═ Is\n║ ╚═ Coming\n║ ╠═ This\n║ ╠═ Is\n║ ╚═ Known\n╚═ The Dark Tower\n ╚═ The Gunslinger", StyleConnectedLight: "┌─ Game Of Thrones\n│ ├─ Winter\n│ ├─ Is\n│ └─ Coming\n│ ├─ This\n│ ├─ Is\n│ └─ Known\n└─ The Dark Tower\n └─ The Gunslinger", StyleConnectedRounded: "╭─ Game Of Thrones\n│ ├─ Winter\n│ ├─ Is\n│ ╰─ Coming\n│ ├─ This\n│ ├─ Is\n│ ╰─ Known\n╰─ The Dark Tower\n ╰─ The Gunslinger", StyleDefault: "* Game Of Thrones\n * Winter\n * Is\n * Coming\n * This\n * Is\n * Known\n* The Dark Tower\n * The Gunslinger", StyleMarkdown: " * Game Of Thrones\n * Winter\n * Is\n * Coming\n * This\n * Is\n * Known\n * The Dark Tower\n * The Gunslinger", styleTest: "t Game Of Thrones\n|f Winter\n|m Is\n|b Coming\n| f This\n| m Is\n| b Known\nb The Dark Tower\n b The Gunslinger", } var mismatches []string for style, expectedOut := range styles { lw.SetStyle(style) out := lw.Render() assert.Equal(t, expectedOut, out) if expectedOut != out { mismatches = append(mismatches, fmt.Sprintf("%s: %#v,", style.Name, out)) fmt.Printf("// %s renders a List like below:\n", style.Name) for _, line := range strings.Split(out, "\n") { fmt.Printf("// %s\n", line) } fmt.Println() } } sort.Strings(mismatches) for _, mismatch := range mismatches { fmt.Println(mismatch) } } go-pretty-6.2.4/list/style.go000066400000000000000000000166321407250454200161320ustar00rootroot00000000000000package list import "github.com/jedib0t/go-pretty/v6/text" // Style declares how to render the List (items). type Style struct { Format text.Format // formatting for the Text CharItemSingle string // the bullet for a single-item list CharItemTop string // the bullet for the top-most item CharItemFirst string // the bullet for the first item CharItemMiddle string // the bullet for non-first/non-last item CharItemVertical string // the vertical connector from one bullet to the next CharItemBottom string // the bullet for the bottom-most item CharNewline string // new-line character to use LinePrefix string // prefix for every single line Name string // name of the Style } var ( // StyleDefault renders a List like below: // * Game Of Thrones // * Winter // * Is // * Coming // * This // * Is // * Known // * The Dark Tower // * The Gunslinger StyleDefault = Style{ Format: text.FormatDefault, CharItemSingle: "*", CharItemTop: "*", CharItemFirst: "*", CharItemMiddle: "*", CharItemVertical: " ", CharItemBottom: "*", CharNewline: "\n", LinePrefix: "", Name: "StyleDefault", } // StyleBulletCircle renders a List like below: // ● Game Of Thrones // ● Winter // ● Is // ● Coming // ● This // ● Is // ● Known // ● The Dark Tower // ● The Gunslinger StyleBulletCircle = Style{ Format: text.FormatDefault, CharItemSingle: "●", CharItemTop: "●", CharItemFirst: "●", CharItemMiddle: "●", CharItemVertical: " ", CharItemBottom: "●", CharNewline: "\n", LinePrefix: "", Name: "StyleBulletCircle", } // StyleBulletFlower renders a List like below: // ✽ Game Of Thrones // ✽ Winter // ✽ Is // ✽ Coming // ✽ This // ✽ Is // ✽ Known // ✽ The Dark Tower // ✽ The Gunslinger StyleBulletFlower = Style{ Format: text.FormatDefault, CharItemSingle: "✽", CharItemTop: "✽", CharItemFirst: "✽", CharItemMiddle: "✽", CharItemVertical: " ", CharItemBottom: "✽", CharNewline: "\n", LinePrefix: "", Name: "StyleBulletFlower", } // StyleBulletSquare renders a List like below: // ■ Game Of Thrones // ■ Winter // ■ Is // ■ Coming // ■ This // ■ Is // ■ Known // ■ The Dark Tower // ■ The Gunslinger StyleBulletSquare = Style{ Format: text.FormatDefault, CharItemSingle: "■", CharItemTop: "■", CharItemFirst: "■", CharItemMiddle: "■", CharItemVertical: " ", CharItemBottom: "■", CharNewline: "\n", LinePrefix: "", Name: "StyleBulletSquare", } // StyleBulletStar renders a List like below: // ★ Game Of Thrones // ★ Winter // ★ Is // ★ Coming // ★ This // ★ Is // ★ Known // ★ The Dark Tower // ★ The Gunslinger StyleBulletStar = Style{ Format: text.FormatDefault, CharItemSingle: "★", CharItemTop: "★", CharItemFirst: "★", CharItemMiddle: "★", CharItemVertical: " ", CharItemBottom: "★", CharNewline: "\n", LinePrefix: "", Name: "StyleBulletStar", } // StyleBulletTriangle renders a List like below: // ▶ Game Of Thrones // ▶ Winter // ▶ Is // ▶ Coming // ▶ This // ▶ Is // ▶ Known // ▶ The Dark Tower // ▶ The Gunslinger StyleBulletTriangle = Style{ Format: text.FormatDefault, CharItemSingle: "▶", CharItemTop: "▶", CharItemFirst: "▶", CharItemMiddle: "▶", CharItemVertical: " ", CharItemBottom: "▶", CharNewline: "\n", LinePrefix: "", Name: "StyleBulletTriangle", } // StyleConnectedBold renders a List like below: // ┏━ Game Of Thrones // ┃ ┣━ Winter // ┃ ┣━ Is // ┃ ┗━ Coming // ┃ ┣━ This // ┃ ┣━ Is // ┃ ┗━ Known // ┗━ The Dark Tower // ┗━ The Gunslinger StyleConnectedBold = Style{ Format: text.FormatDefault, CharItemSingle: "━━", CharItemTop: "┏━", CharItemFirst: "┣━", CharItemMiddle: "┣━", CharItemVertical: "┃ ", CharItemBottom: "┗━", CharNewline: "\n", LinePrefix: "", Name: "StyleConnectedBold", } // StyleConnectedDouble renders a List like below: // ╔═ Game Of Thrones // ║ ╠═ Winter // ║ ╠═ Is // ║ ╚═ Coming // ║ ╠═ This // ║ ╠═ Is // ║ ╚═ Known // ╚═ The Dark Tower // ╚═ The Gunslinger StyleConnectedDouble = Style{ Format: text.FormatDefault, CharItemSingle: "══", CharItemTop: "╔═", CharItemFirst: "╠═", CharItemMiddle: "╠═", CharItemVertical: "║ ", CharItemBottom: "╚═", CharNewline: "\n", LinePrefix: "", Name: "StyleConnectedDouble", } // StyleConnectedLight renders a List like below: // ┌─ Game Of Thrones // │ ├─ Winter // │ ├─ Is // │ └─ Coming // │ ├─ This // │ ├─ Is // │ └─ Known // └─ The Dark Tower // └─ The Gunslinger StyleConnectedLight = Style{ Format: text.FormatDefault, CharItemSingle: "──", CharItemTop: "┌─", CharItemFirst: "├─", CharItemMiddle: "├─", CharItemVertical: "│ ", CharItemBottom: "└─", CharNewline: "\n", LinePrefix: "", Name: "StyleConnectedLight", } // StyleConnectedRounded renders a List like below: // ╭─ Game Of Thrones // │ ├─ Winter // │ ├─ Is // │ ╰─ Coming // │ ├─ This // │ ├─ Is // │ ╰─ Known // ╰─ The Dark Tower // ╰─ The Gunslinger StyleConnectedRounded = Style{ Format: text.FormatDefault, CharItemSingle: "──", CharItemTop: "╭─", CharItemFirst: "├─", CharItemMiddle: "├─", CharItemVertical: "│ ", CharItemBottom: "╰─", CharNewline: "\n", LinePrefix: "", Name: "StyleConnectedRounded", } // StyleMarkdown renders a List like below: // * Game Of Thrones // * Winter // * Is // * Coming // * This // * Is // * Known // * The Dark Tower // * The Gunslinger StyleMarkdown = Style{ Format: text.FormatDefault, CharItemSingle: "*", CharItemTop: "*", CharItemFirst: "*", CharItemMiddle: "*", CharItemVertical: " ", CharItemBottom: "*", CharNewline: "
          ", LinePrefix: " ", Name: "StyleMarkdown", } // styleTest renders a List like below: // t Game Of Thrones // |f Winter // |m Is // |b Coming // | f This // | m Is // | b Known // b The Dark Tower // b The Gunslinger styleTest = Style{ Format: text.FormatDefault, CharItemSingle: "s", CharItemTop: "t", CharItemFirst: "f", CharItemMiddle: "m", CharItemVertical: "|", CharItemBottom: "b", CharNewline: "\n", LinePrefix: "", Name: "styleTest", } ) go-pretty-6.2.4/list/writer.go000066400000000000000000000007701407250454200163020ustar00rootroot00000000000000package list import "io" // Writer declares the interfaces that can be used to setup and render a list. type Writer interface { AppendItem(item interface{}) AppendItems(items []interface{}) Indent() Length() int Render() string RenderHTML() string RenderMarkdown() string Reset() SetHTMLCSSClass(cssClass string) SetOutputMirror(mirror io.Writer) SetStyle(style Style) Style() *Style UnIndent() } // NewWriter initializes and returns a Writer. func NewWriter() Writer { return &List{} } go-pretty-6.2.4/list/writer_test.go000066400000000000000000000030451407250454200173370ustar00rootroot00000000000000package list import ( "fmt" "github.com/jedib0t/go-pretty/v6/text" ) func Example() { lw := NewWriter() // append a tree lw.AppendItem("George. R. R. Martin") lw.Indent() lw.AppendItem("A Song of Ice and Fire") lw.Indent() lw.AppendItems([]interface{}{ "Arya Stark", "Bran Stark", "Rickon Stark", "Robb Stark", "Sansa Stark", "Jon Snow", }) lw.UnIndent() lw.UnIndent() // append another tree lw.AppendItem("Stephen King") lw.Indent() lw.AppendItem("The Dark Tower") lw.Indent() lw.AppendItems([]interface{}{ "Jake Chambers", "Randal Flagg", "Roland Deschain", }) lw.UnIndent() lw.AppendItem("the shawshank redemption") lw.Indent() lw.AppendItems([]interface{}{ "andy dufresne", "byron hadley", "ellis boyd redding", "samuel norton", }) // customize rendering lw.SetStyle(StyleConnectedLight) lw.Style().CharItemTop = "├" lw.Style().Format = text.FormatTitle // render it fmt.Printf("Simple List:\n%s", lw.Render()) // Output: Simple List: // ├ George. R. R. Martin // │ └─ A Song Of Ice And Fire // │ ├─ Arya Stark // │ ├─ Bran Stark // │ ├─ Rickon Stark // │ ├─ Robb Stark // │ ├─ Sansa Stark // │ └─ Jon Snow // └─ Stephen King // ├─ The Dark Tower // │ ├─ Jake Chambers // │ ├─ Randal Flagg // │ └─ Roland Deschain // └─ The Shawshank Redemption // ├─ Andy Dufresne // ├─ Byron Hadley // ├─ Ellis Boyd Redding // └─ Samuel Norton } go-pretty-6.2.4/profile.sh000077500000000000000000000010461407250454200154600ustar00rootroot00000000000000#!/bin/bash # cleanup the profile directory before starting rm -fr profile # profile each supported package for what in "list" "progress" "table" do echo "Profiling ${what} ..." mkdir -p profile/${what} go build -o profile/${what}/${what} cmd/profile-${what}/profile.go (cd profile/${what} && \ ./${what} && \ go tool pprof -pdf ${what} cpu.pprof > ../${what}.cpu.pdf && \ go tool pprof -pdf ${what} mem.pprof > ../${what}.mem.pdf) echo "Profiling ${what} ... done!" echo done ls -al profile/*.pdf go-pretty-6.2.4/progress/000077500000000000000000000000001407250454200153245ustar00rootroot00000000000000go-pretty-6.2.4/progress/README.md000066400000000000000000000017401407250454200166050ustar00rootroot00000000000000# Progress [![Go Reference](https://pkg.go.dev/badge/github.com/jedib0t/go-pretty/v6/progress.svg)](https://pkg.go.dev/github.com/jedib0t/go-pretty/v6/progress) Track the Progress of one or more Tasks (like downloading multiple files in parallel). - Track one or more Tasks at the same time - Dynamically add one or more Task Trackers while `Render()` is in progress - Choose to have the Writer auto-stop the Render when no more Trackers are in queue, or manually stop using `Stop()` - Redirect output to an io.Writer object (like os.StdOut) - Completely customizable styles - Many ready-to-use styles: [style.go](style.go) - Colorize various parts of the Tracker using `StyleColors` - Customize how Trackers get rendered using `StyleOptions` A demonstration of all the capabilities can be found here: [../cmd/demo-progress](../cmd/demo-progress) ## Sample Progress Tracking # TODO - Optimize CPU and Memory Usage go-pretty-6.2.4/progress/images/000077500000000000000000000000001407250454200165715ustar00rootroot00000000000000go-pretty-6.2.4/progress/images/demo.gif000066400000000000000000003133231407250454200202110ustar00rootroot00000000000000GIF89a@8! NETSCAPE2.0!d,@  3 3 T T s s        8 *\ȰÇ#JHŋ3jȱǏ CIɓ(SN$hP˗0cʜI͛8s2@@JѣH*]ʴ) Z:JիXj݊`ÊKٳ <0 ۷pʝKWb˷߿wXP0È+^|p#KL>`ϠC{ AfѨS^%װcˮz۸sͻ Nȓ+_μУKNسkνOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ*무j뭸뮼+k&6F+Vkfv+k覫+k,l' 7G,Wlgw ,$l(,0,4l8<@-DmH'L7PG-TWmXg\w`-dmhlp-tmx{|߀.n'7G.Wngw砇.褗n騧ꬷ.n/o'7G/Wog=!,  H!?, 44 4 4 W 4W 4v WvW W4 W44v4 W4v4vWvWv22WvvWv̓W v4v4Wv̓W̱v̱̱̱̱̱̱̱̓#H*\8 tPСA!jȱǏ Cz$ H 4YeF0cʜI& Nx3g˝5 JK/#S(@qBŨS74-XÈX e _Qh) 8) | hہzĊDtQ{UڭN},kF \ew*ғO#ˁY TjÑǖ 5H!|FpmWp(p˛3|KF7w۸nZiJO/㻯@ peBݾO{R͇cᷔy8QO=w܀W_Iݹe~᷒a<7 TD"CjH!Bnb`fxe(! L6N7樢Ib?H_xA f|exY%R.#~y$,c[;(\M:ɣchВR;AX@[ DUҩw_upz#q}XUfԽOAb1]-ʜ:xbi@`(XVZa^N_Pܝu~9VfLeX%L5V ,jZ.nAZ$½ #9[LҔo*B 2Q,1nTE-oTYY$ȝuq !L,  22H*\ȰÇ#JH!/VȱǏ C̘Qɓ(STIʗ0cʌR̛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`>@h-mеd ܻ8!7,  H! , H!,. 4 444 W 4W 4v WvW W4 W44v4 WWv W4W4v4vWvWv22WvvWvv̓W W4v4v4vWWvv̓W̱v̱̱̱̱̱̱̓'Hd耰Ç#JHb.\xȱC =#AC Vʜ9Ѐ X0PBh\( "8ѬJf7MHa1,dlaܹ @]K6dL[wF a&$^r!qYg3d/gX-څuxs~{X =܅ K $=CE lG`M_ !H Dx%GPz"6A|9haP 6a]MUF_A VA7R`3 "BdA$}I.^ +(PV>d \_ׇDžH]fؠ(5&9(M]DK&t4 OD|N*OH(B{tAC~j@bJ:姕aUv2:q?%@꣐f)zn(+{ ݥΖinzP w j2{P *{a}ij+PP -.[/FPYߎ6-njnP;o~޹pH  MbzE.L %/3/ W%b+@*$`n[1-.cS?V)[՞|VQE*PXZŸG.yC<_h^砇.褗n騧ꬷ.n/o'7G/Wogw/o觯/ׯ  ?3! F,0   " 4 9 + ++ ( 44" 4 %( 4" W ] "F (] 4W 4v F^F ^ uu u,B %B93X9 Wv,B],X]"Ff4Wf4Wv3m]:m]W F" W4 v4 fF"vW4WWvFfvWvvfWFvfFvvWfvvvvfvvv ( WB BXXm4W3m4v4vmWv22:%,:3::%,,3:WWvWv̓W W4v4Wv̓W̱v̱̱̱̱̱̱̓̓H`2i`)Ȱ :Hŋ "TǏ A ɓ(S\ɐ\JN@F"H6Yđg-+q4$3(@bǢ2Sb 6jCW(x5PJ9"mB:-hHv(·%`#$YӤP# q.#_|2QPI܌PWNZ |h#gM†[#:xUE\޼(G#'>]г\m{G*\\0x]Or#}xFiA]! hti a$DytDi@hM>!šMPQLPEBhl!YC!b(Rm䡄O Q אK 0T21a 9| E8 K _PG`XQI !@r('`~v|ePtb,T}dgP|@䞢$W)%]ʐy`h2T:xj5PⅱJ+P:jTVXPE;0!Ia%/F -9@uVWlaCve!ݥkNdQ=@NLפo8&½ZC |KtDSUOJĚ+]wn5uwJ|g[6kC, iI7- @1֐8Klb,Oybh8/3gxb^ P}8 P@Ї8!24PmHҠu'Q2@ECbJfF+D#otԡfTD'E+QOE~gcqWĆ H"jA#Q>ؼ/ _@p xZ!侥 #H%F%J ^䕲/F <&/̌34TXdhZFJVn M6Ȗ7IrL:v~ @JЂMBІ:D'JъZͨF1zT  HQ(eI;ҖH+KgJS!S,@]   " ( 4 9 ( 4 9 * * + ++ ( "?" 4 ; ; (;4%( 4" ;"? W ] ] "F (] 4W 4v;-K F^F ^ uu u,B 2[3X9 Wv"Ff4Wf4Wv3m]W F" S* W4 a a a p* v4 a6Ka6VSqfF"vW4FfvWvvfWFvfFvvWfvvvvfvvv ( W BXm4v4vmWv22:,3:3%,:WWvvWv̅ -?" ?4??6K?K?V?VW W4[ H?v4HKHV--(6(?46(64?4H?q H?HKHVHKHVWv̓W̱v̱̱̱̱̱̱̓̓H`H()Ȱᔃ :Hŋ "TǏ CIɓ(S\ɲˏ %$DGAc@)9SDHLJj(` @u*Pz5kb = 2c۷p2vEG!-[b x )(!BE+Rpbʑ#HL9Ģ!QWl * ZjE'eO9j/?x44Dyp$P3"ŗѺ1ν+V*>da1H $vń@';@ QL ?1FyԠCEZH1Ԅ!qYr!|Ր!X"e(`vh݇4h@U@h@J\A@K4&cc]t S`dH:CQ{5# !aqSR]t@$^$4ITyL5 Rmf8DP љ6*X_Mꐤb:x!bE=lw㨤x; ʪCBL0 䑇ENA_d`#5m&::mAT@AZ5FkKkRZ^֨7qP0XQEj+Jy!aOPANt@Dy*琕19P1&f1kR%h`AVې95iR[ .J5CVDWK!U 91 O2l0 Ô ~!Cc!i<y $Str+ :,= lEA첐HdHn. Q8PdS|=:OE/N͛L-{MKҢ:hmim@_GGc8#g_jɫ?MZD%A _St^@v K𖆴|k@u Y Xu*%y ES$2C> x s=S^C7h APVM}4O~>S~"@AD&nyk zO?-N! (@ $*s׻yIb? C8љ } o$&Cp Iz*bq c,'QrJb"̯0B 8%!xO $!H6?%O%%KI ]HH0 Q8Oa` \$0)hnwkHҀ@ u(DЇFԢE'*rt ݨH3юj )JGjҒ-M)L_RƔ!,%X!B:CP:$?Q 3H!eTdc+,S` $)F =ZkS\r=jX ׁ uk\Ҫ׾y_S"׹SXe-midG2%/{,Ibui hGKҚMjWֺdlgKͭnw pKMr:ЍtKZͮv݋TDwK$5z׋]~s+^ ![,@e.   " ( 4 9 ( 4 9 * * + ++ ( "?" 4 ; ; (;4%( 4" ;"? W ] ] "F (] 4W(] 4v;-K F^F ^ uu u,B 2[,B93X9 Wv,B],X]"Ff4Wf4Wv3m]:m]W W 4F" S* W4 a a a p* v4 a6Ka6VSqfF"vW4FfvWvvfWFvfFvvWfvvvvfvvv ( WB BXm3m4v4vmWv22:,,::3:3:%,3:WvvWv̅ -?" ?4??6K?K?V?VW [ H?v4HKHV--(6(?46(64?4H?q H?HKHVHKHVvWv̓W̓v̱v̱̱̱̱̱̱̓̓̓H*\ȰÇ#JHb*(XȱǏ CIɓ(S\ɲ[Ȝp&M졙F&Lr  ̠YeSpt+MOa`Z%@سZLDgm`(Wܹ%Po̺weF È7^:+AĠ2-".>9ZuK2'7AXKdq N6E5l8vm:I<}KNb| bsgt NgM!Pə&QڵlZ[u 4[w}gq Qe`gtF((d!$p'2!`;1@aCNAANZ$3*O T m5 =N%A(_XYdEdvmFaCDd :rfm&^n"c橧9QBidl}"چL,6@I(`T `gI^CQQY%4%Z|՗_f@c!$\`9gjf:˜p"k f-\Or@u{LtGwfD(@$'@"MEVAB@qƌJ1Z2$B:i+qyeٶ Kgsנ; )؂,!.6oQn.@+wˣ@G3zI{ 5OF`n_ƷaVa~mɍlʪFw2 *6x-Qz+rdnۛ^ -͞VU-$u®VBz~淟f@|uB,lܱ&ȴ *j7Zm Y`edhA~վQmd o1F׵$zYV6ޫMX_ˎ{о] ``1dKɠf"@N&r(! ՜#,_~rȄkbRd~BD: wʌXjЊs'.z:IR'Q$w;ؑ0$dȑ4xOǍ f>BѐL"F:򑐌$(FSI ar (fO";HIbpIa-`hE e/An؂ !F@8pB4L^JAY?Z~bNu,#_ "k 4';"K * }\aCtx!|;A!E ?h@[Ej\7{h  W_6a˂ZĦ)ׄ=3%0aeTTKo Ї8DhA-Bq@&"J]-qILE2 JXU(F?K݀&@jujnH@,! QlP! mA@ͲҕD*i(,&nr ׸vq*ktt4F ~ dViAXҒż*5^:3נUP@($jd?u.!lw#y'Q@!z BX5)la SpDCPR QnBŅQa`SLyB.W }R!,:tu+cI P=XslgO ny2B˛ɗ=Q;ՁxBK"5SB͆+| dέD!Oh{KИUܛ!BȫG!qYblAj"X9<Dp! Wmoe2CfF3l6R{mw[;vHP7{ܗT;Vc]M!BtOӟƂH6:bQ5\bx5vV.95Ђ4<x-QՇC!WÊb;ۺ6Bg=-<&7&iLNbCnn91:IOLPd_)Q*xJ3\T=UEM')Bо-!{6q"hNY)hmFOmsN x}E^_ɰ"@"6]y@o(2u;`8srOzB$EC6 qh|6"%ЇC` 'D t/1AЈI`?@o~wo?OwGxXwQWMNNdA BMBSs:tSZ>:CaaCgx>D=&D[;gw4Sw<7Xnd\8H2xPxJ83AHCWE8H3zM$MY-քMWԅfxhjl؆npr8tXvxxz|؇~8Xx?؈8X4 `؉j艢8H c!a,Pe.   " ( 4 9 ( 4 9 * * + ++ ( "?" 4 ; ; (4 4;4%( 4" ;"? W ] ] "F (] 4W 4v;-K F^F ^ uu u,B 2[%B9,B93X9 Wv,B],X]"Ff4Wf4Wv3m]:m]W F" S* W4 W44a a a p* v4 W Wa6Ka6VSqfF"vW4FfvWvvfWFvfFvvWfvvvvfvvv ( WB BBXm3m4v%m%m4vmWvWv22:,3::3:3:%,,33:WvvvWv̅ -?" ?4??6K?K?V?VW W4v4[ H?v4HKHVvW--(6(?46(64?4H?q H?HKHVHKHVWv̓W̱v̱̱̱̱̱̱̱̓H*\ȰÇ#JHbA,(XȱǏ CIɓ(S\ɲ▌aȜp&MƓ'NrDvhc0Ki.S0)J`  PaVEڈx[}ǟЩ*W+v!CwL5WʗpL̓l0&8 9tR0ᦾNzr%$cNH#NrR 9%*WJ)!LBXrD̥. e$#H |2f:Ќ4IjZәQ R+qnz%8 6$"@ >NuHZ`#*{saP ,q0=PH8Nad@&* '$(Yl xDB2'A! .irN5} T1`D# iYgA UL GaMU(! JTT3JQb D@4;OU $r&BzAJBdLiz#v*'U_ݙU U" V B$zB6 F`*WWU'3YB5Cyekv#:@B m$H I0m) !&iLx[ רH$.P &aA8" Q>$b$3")6-ʉs>tʣJ4EH;n}hG+҂UaBvea M?YSCu:=ÆS>(@i{y 0id|?UWdp8#1f *[U"[ߕH!k.9c%a ֫g~ʃ @LVB|ңlw׺IO-YWՄ{%H޲b!ɛH_ {zv4rAj_: [i^SX!"Qkˎ13L(P H!oJgP g%eBj8i%*4.hvN2=UFҵ"yʪVjga Rr.֠!tmBy O|k߆%ò򃴼#XksI1y񛠛7b/zBtǖqZ99J&HqR&l_V?_5M;xBd lAvc,"`B6s>{)lmݬtOpJm0Oo I PoIqz.Qc{<]vCk"&N7l9Փ5 XabĶ9O]jVaz6|l+~Wi0TXfEBX! p8v-Hr?w 4j0PTEuڔ rHDVv bDc]g\#kw{f|R^C{s7I <fzW!p +818%0Oז0 ?0;Pfavmg :ЄOP(WXST(V؅a\8Z^ebdxfhjpk(~tZcep\d^jl@wc#RR`7(D9Jx}}?zX1'N(J1r/sJN.:8]HP P¨/EQK&Lv8Xxؘڸ؍8Xx蘎긎؎8Xx؏ 9yD ِ9I!h,a-    " ( 4 9 ( 4 9 * * + ++ ( "? (9" 2 4 ; ; (4 4;4%( 4" ;"4;"? W ] ] "F (] 4W 4v;-K D F^F ^ uu u,B 2[3X9 Wv,B],X]"Ff4Wf4Wv3m]:m]S W F" S* W4 W44a a a p* v4 a"4a-Ka6Ka6VSqfF"vW4FfvWvvfWFvfFvvWfvvvvfvvv ( WB BBXXm3m4v%m4vmWvWv22p:,3:3:%,,3:WvvWv̅ ""(-?" ?4??6K?K?V6K?VD W W4v4[ H?v4vWHKHVvW--(6(64?46(64?4??H?q H?HKHVHKHVWv̓W̱v̱̱̱̱̱̱̱̓̓̓H*\ȰÇ#JH"BA!<! g BU*YK41mҔy3L5(ϝ:Ut(REMjՏ[x ; (\X4d͢!G1j΢1!ݻ$r̄#9BgnA #>qTXqbϠCMpI& ]X@I(P#ć$L8){71y{Jᾁ/8ĕ?g9үON}9-} 94%E pңi>ڶh߻~+A ^pF `bF`)@/10Tg($XP!P ! !%\@0Ǝ=#>Y$B$ dJBɤ@QR9G>iX.e&&_dhCBl mqy4cC 1@$_aYc: &hDi饘fPjat50֕$N4*Ȫj무zZ++"[l*Ut]qw@n%@ -[6aY!CfVDq\~ѡr"W/f !%&@>'":R}BK)F !\)l2hr+,316s4Ϝ3C s;BG¹y,t&ج ˂. vp !]U,Mp:@-jj˝ӟxo %_?q^ Z )0Pb'Z#Ҙ@Q hb*ECFtg;f,csX6b'pt#, #BaV8 ?~S[`,z7LtHR_( 0KL(I/";A$P1pF tt%0ٲR.K/RJ /4LTh[g2wH>y{y.b"E|#$obhDrR.ؐ٠E|\)~BЈ&P$;Hj */3"JLEыV4F;яzT"MICґ(m?@MNp2tI BMxdf 7E ` AgS Qq2'fO)o?J׺xD#׼ `{Mb^1fKZͬf7z-rHMjHA-Q@ٲ"mp DDhmFׄq6hH@ &*[?0 V5I!B~0+qAKY*@FB+b1~{˰k#*D[~@h` Y!D`.C""Ń#L\B:{J!XM D&hTwwn>왦} XL)E bCY C"[Ѽ)[XȱI;`˳y*gz'lll!=gmFtm0{`f{{{wH6w6dc;/ko8N P$E%p&}xGwypGxGsg`wY&W~=ȃK8aq2f `x1 Cbȁ4u4Vu!&VHyVuhD&IH$?R2(4d4)u7:8ŃVvwpOxy ǃk (a&yYr[ ~[s&QBb ba&JvPdžm.nLvƷM.Ng IVV4xO!`q5 EiXV 6VzG腆7ZVxh Hyski>8 Ȁisq)ipQ\\ %$y\QN!P{l& ;ٓ:ɓB)D ?9AEKIiO)QUyPٔXIZ9VٕW[ᆻ{_&F^TVUϨDf _vc_2Vg@[uVfyWQd_f`dX\H3JfhnWII@vTxove~YBօ ]%g1^o&YG9Yyșʹٜ9YyؙJٝ9Yy虞깞ٞ 9yEٟ:I! G,`.   " 4 9 9 + ++ ( " 4 %( 4" W ] ] "F (] 4W(] 4v F^F ^ uu u,B %B9,B93X9,B],X]"Ff4Wf4Wv3m]:m]W F" W4 v4 fF"vW4FfvWvvfWFvfFvvWfvvvvfvvv ( WB BBXm3m4v%m%m4vm22:%,,::3:3:%,,33:WvWv̓W v4Wv̓W̱v̱̱̱̱̱̱̓H*\ȰÇ#JHbA aXhCIIR\ɲ˗0cʜY2e!\hsg[Lx +tzRǔ+WP0+"PՀR~4)|ړPZ=#-J\La3lp师)#SϠpQЀXH*YG<. %tY͢EYx8s" 0A+wj Z=p=LYjl&Yga fi ZaD f4TlU`~%6R~Z@9Pt*iO `8xV$Gh ;qЁ@x_ч}F3V8[e y \$DZhUmpZb9_hTfzy i!Cd#Ui rpY#PAi,fx)`c76מM EzԗEEzv5MWVʚ٥.K_~ l "j+P`4gF䚫A[ ؤhCDsαCGTL,`V88qZ, ;l9nX1z0/VnU+Gip^^b^)e~X&V+aB?[ r怆 7e$5@P" [id@NN.Ka\jo+dU0*[Ѐ:k9O0` Cؤk^A3@  .Bt I.ש 0Aңb'&nq=!@Ђ-:%Z",kh9QAtTܥHG*g-$Ő@ jP9T)cJӚĤAMWRtt/N8ӡ)Q՘!O@hIM}Uծz` XJֲhMZֶp\J׺xͫ^׾ `KMb:d'KZͬf7z hGKҚMjWֺKlgKͭnw pKMr:ЍtKʮdb ov+J7 !e,j4   " ( 4 9 ( 4 9 * * + ++ ( "? (9" 2 4 ; ; (;4%( 4" ;"4;"? W ] ] "F (] 4W(] 4v;-K44W D F^F ^ uu u,B 2[%B9,B93X9 Wv,B],X]"Ff4Wf4Wv3m]:m]S W W 4F" S* W4 a a a p* v4 a6Ka6VSqfF"vW4FfvWvvfWFvfFvvWfvvvvfvvv ( WBB BBXm3m4v4vm22p:%,,::3:3:%,,3:WvvWv̅ "-?" -(?4??6K?K?V?VD W W4[ H?v4HKHV--(6(64?46(64?4?KH?q H?HKHVHKHVvWv̓W̓v̱v̱̱̱̱̱̱̱̓̓̓H*\ȰÇ#JHŋ3j1 ?vIɓ(S\ɲ˗0c2̛8sɳϟ@ RѣH*]ʴSDE>JիXjʵׯ`ÊKٳhӪ]˶ۦyk)Cn(uA_e.<b‰ C~Xrƌ%HSdž,rQ@`9)ZG1UU+H8- xa5?n<9 0q+VlA= QL&"__>/S??|_yUEp6Pp@E 4AT[D[hD"tR scPPdYƍqW1(%G桇TKPZKete}az gI&k_cɦn 4 Z|&R%B$h2\AGCnЌ+B Tw%WFpGhi Ф>%V&E( 6a= ~J  .aⅰx H_z ]yWBcKqp!<6($"La٬ d e36 k:Yۡs\l"{tskY#9AkڂpLVe%XBA͍=~DCh4=b(j]B*m ;XCڶerɫ:`p<7`;6AК+Im9 `KM2CDFuas Eo@Nwٛo,91eAu @V9<^NVdDNyYO @(@\>]&jlNYi>tnO tEIK!>LW> A>|8':J ުG3BI TsBЈ2 ?qa #nwzGĐ? <"'_V<׾/(g|J"\X2p;aG`XQ^,A{TbcvW]t79Bv ccpY Qi$kQ8`<[vm&T}y!(l7xطkJ[*Ui&trCL&mdUD 9]ͅ] UJzv)!hH^Zg^I`Wpⵀ==DljoGj$K|+1UU5qvey`gytx7T#{\7 `eq i6P4X\eC75h`k#kwghe9X%0 H؊wW G0h oeNWbT{ 0qɸȨ(hXՈH؍8xH쨎SU:Yp@N OVOwE7$ E P Y7u0vRg%QiXhZBvHHX!i #1Qs`g.e XbYZ>yHJLٔNPR9TYVyXZ\ٕ^`b9dYfyhjlٖnp 0t qyxuIyٗ~9{99Yi! g,p>    " ( 4 9 ( 4 9 * * + ++ ( "? (9" 2 4 ; ; (;(;4%( 4" ;"? W ] ] "F (] 4W 4v;-K D F^F ^ uu u,B 2[%B93X9 Wv,B],X]"Ff4Wf4Wv3m]:m]S W F" S* W4 a a a p* v4 a"4a-Ka6Ka6VSqfF"vW4FfvWvvfWFvfFvvWfvvvvfvvv ( WBB BBXXm3m4v%m4vmWv22p:%,3:3:3:%,,33:WWvWv̅ -?" "?4??6K?K?V?K?VD W W4[ H?v4HKHV--(6(64?46(64?4???KH?q H?HKHVHKHVWv̓W̱v̱̱̱̱̱̱̓̓̓H*\ȰÇ#JHbA02XȱǏ CIɓ(S\ɲ%G0x&fL6a uTBaIcLwvpRyFL,u0DŽ 4V(c̋{ 3&2˄9&xkA dlu"LMi7gf \=j 4.CC53ر5ߦPBNfA.;޹u@g"i!pEʐEO?ٵfƠ!oy>~'ND vMFV]L5Xs961bNНe9(^"A.pfw_x_U_;W(Q!Z9 `)B& .ФgyҦFdĄvEVfAJ>3V~{쁡n:>S:ˀ#ELv]y J;Yj"Ah: t"8;@1~yoz&̋X}/>!f}lC܇\CٖLE.sP5Jt oG9$ 1wi e*b A GÃRcSa ĝp/d&+b=lY= YNl/a&!|̚`WlnOLU;uHi0LgJӚ8ͩNwӞ@ P*:$⨓c8pPDA DX)PEbUjWVUիc-WֵuD w``9rZ6oe ʅǺz2>Ĥ(hH<&(E 4 $!G b@ -nk -py{U 7-H,@]ޒMQBTDIC);Q3Z/t6 $z{B\*5@l$PRBF W 6pnϐ3n%\+G/dt. F{.t[pcq Toj>3/~`.De)GVr,w[rdfxtԋռ26ˤA< 3w&㑹p:@-UK]<{ y@R M5DPVL ㍸%Hl -T "ɞL(pBj0<j"D3.q"pr:T}d3ۊiQHCps`ͭnEUK8CɺT{D#.V;\qLwB@M1b׽v-c?{tfk6H)pF*VnmѬ!m28Za1* g6mEW/ >.qAy&?-?&^Q*hg_P|W0e0V4 qK*Ȏ@,z2! w$0'<~ ?/\W:) aũX9ECw9g7$|r!t:1${$yxAGJGtN}TWi~$pTTA i `gЂ0h`2481X"(cplK$tDŔ'(EK37LUL<^a&fGh~Md=nxY)|{$Ne*ׁ?|؇:p!A}~8X)u"!؈v2*Z8Xxxr 0x&P艚xh?e2' :r;A3Q4hAp $_ 0XKe`q0 QVa REKQw%!YY X=!:É.xh4 A{ 4@@wZaiO K`X2`QbAQyVsAtoAi2.!A) a(489! PsVdgyhE)Hpp̈9'X'v&gc$xh='. Oc'!` g-(gmg;>S.qŠb'I Ɋ{y&oɓ! u)YwiIj @Fyɑq;zwgz)Cg 1h8h 11x*Bx3.{` #)OD!4).hH."R.aw 4F Cy C}}ٞ}`iX,m$A ӁFBolxG$HbooS3é6y2ٜh6II*z:!*!4I/z)쩣id}U6lys8}xAG{t#k*yQ'oqf215'꜐iӝx< sX0Jpt9$)U򩨚ɨ( NIVi wM{!Ng>NPGgI-u١!i,=   " ( 4 9 ( 4 9 * * + ++ ( "?" 2 4 ; ; (4 4;(;4%( 4" ;"4;"? W ] ] "F (] 4W 4v;-K D F^F ^ uu u,B 2[%B9,B93X9 Wv,B],X]"Ff4Wf4Wv3m]:m]S W F" S* W4 W44a a a p* v4 a6Ka6VSqfF"vW4FfvWvvfWFvWWvfFvvWfvvvvfvvv ( WB BBXXm3m4v%m%m4vmWvWv22p:%,::3:3:%,,33:WvvWv̅ "-?" "-(?4??6K?K?V?K?VD W W4v4[ H?v4HKHVvW--(6(64?46(64?4???KH?q H?HKHVHKHVWv̓W̱v̱̱̱̱̱̱̱̓H*\ȰÇ#JH"JO#0(Rdœ(She!,W|Kidެ9N9m3ѣYʣD^6@gРy1@ΤiAu֭]~ \h ܴV˷ߔPx@ Q  *]JXp#G\"*-c,s4;ҳХ%PaM fY ¯X \qBߺ>M~Pk%z5F <>sߣ}_Zdv@qПZkUh}n !oi5@hH@ hGZidǹ#|ʍ8Fb#7c;C dGHU\G ]!GTIixtg@U_El(&jpZ(BC)g^r4"Pz.*(ViS]Gd1BW)Y]a(yZ @ֆiWl)wf )²uM+h/QG`냶z nF;g uꨥۮE!k6г"LQSl1oq) >d@'Ûa i GYgE2'sCeFKZMOt DB'}kʊQefA!YX-wC٬%s#_~g_Y֦.DіF |`i0A.t/ޭ?$Y)U1ڻضiҽ/1;Acݰ\{)v 6vi(QSLxA y ah;!Oh~APp)Tq xWfլ+r ZI4ztZ35o9Xa3Z{!Ўyζ-ڟkk=C+Nv /,z}1 ӿeqX">ț_С#h"'$i`#rCL!K+k8 7eBi* Z(f62I'z d$|U/a1H H?}A1}1Jƒ d +9U  o gdI]Q e}|1} UXF؁WGu Rcx~gVXE[uuGGt^ȁ ؆ H ȅ u( 01NEpdF g -{/xwAs@'*b*)U+VGDWc[ rvvnW%tx m} C`7zuiatAz](X'hgLwtV(uyqtX QpxXP Kv{wM.^i@kB\Xx !Evfg%l{('@~w~eo~XHXpVt( 8 z+ȅ>Yhx7Vz+)2 A.5DSbyf_#eM8),c!)`&3"$Jia~159gX~h=ɓHyGiKy}qMYV7 z~rz` Dva9'QV"gZEfw'gɋ1?Y 8ܩHIy z}IywHu[Hюhi8}zfavd0 vdP_yNfIha0Tiw) !>ϗ_xҜhgY_At,6` qiק ާo_Y&II}(i I홆 'o&1~ a!d*~cZ 1q$1 F0l*`iH{h ѧ~ !*JzʨꨍJ:Zzک zHyCn*fg6+ga5SM+*V̆|/zY; hG:?za:bZ{תz?j7]'oJ! #W qr)VrTnҩ[{ ۰;[{۱ ";$[&{( ,0K.۲1[ ;,k:˱8;!k,N    " ( 4 9 ( 4 9 * * + ++ ( "?" 2 4 ; ; (;4%( 4" ;"4;"? W ] ] ] "F (] 4W(] 4v;-K D F^F ^ uu u,B 2[%B9,B93X9 Wv%B],B],X]3X]"Ff4Wf4Wv3m]:m]S W W 4F" S* W4 a a a p* v4 a"4a-Ka6Ka6VSqfF"vW4FfvWvvfWFvfFvvWfvvvvfvvv ( WB BXXm4W3m4v%m,m4vm22p:,%,,3::3:3:%,,33:WvvWv̅ "-?" -(?4??6K?K?V?VD W [ H?v4HKHV--(6(64?46(64?4?KH?q H?HKHVHKHVvWv̓W̓v̱v̱̱̱̱̱̱̓̓̓H*\ȰÇ#JHbA20XȱǏ CIɓ(S\ɲF0aɸ&fL6a` }hђ{(mc`͂2J 0'UJ0uF鏩HeDtKR?xRijNmd.Tẁ|Z{R\&|`:;~ j\ 8+M31zpN= `8خ!k`A ޼6J! /x8rGz=^#HeUi"DHc~@k] ħ rе֓n_hqv@Et`~Vh!HlۄPAskxaUN(x@X[I@eb>>M/-PHJ2 w8qv 2wIBـ%W ` 0BɊF ' ?1$"AqM'o6hiuH]L^'V LpUXPiNN&$:+L.Fizuǡ%i^& F`}Yi^Kc }f$.든 L6(A5AMfx@ԥ%nXSKTSD!}]k8WPڐs.q 52t|xLĸ|kO$'t2ASXzH{jL%&) vsꔥ5h2E*XںG n 0 wFd3 G5Aa#դ2x[nTBlb:I\Mg"I%kAMXfycC2f8mˆ5c Ov D82Q"ʐ%/\kH0); q*IrNI:kPS\ Dk#uG=Jd5 s%X'.6\@.]Z8FTv13qi% x^qPtZj'$xlCDp3QEF-I ¥d b[X$ R "K gXt?G-.?f]ÍXR!i-E8Bb> DjSn3kx{g=t>k@HJ}؇+T+SZQQ(mm{ZXhhhhƈ*(_0HAMai{7 3ew(318j1}a?Ib39b8-D0c~Nĉpcj;kkl1!B1$M[Qysi #)((c>}(C+b#9J:.Ȩ*(^p=eՁP=6"K(ӎDRx6D1^~h9!Ò29~Q?WJAXXZ\71wo'wa< 8''* "rB'v* (' cZ*tB AZk6 `, =)vqt*8uJh+D6餥A/*.⒭Y3++/+4T58.ѩɃ^:ѳ@˲* 5*J1O p[qY`pij ktDiH! +.yv;$E"͚5ʳ7 F;K5bAM[.5.K[ Gj<RK.:+C7{|L@.sLl~ q|RH41ĚƇkI~L'Rۺ 7p Ƀ jl{L< Y,(D',/ۿq& <|jχ# [pVekk E@-=m-! m)%}$+*45-6m̻<'$O`~}\)~udչ{xpL&RFG|l95[lwF Y|!BG%t®<- /ڞ2J28*[Mz؎ْؐ=ٔ]ٖ}ٜ٘ٚٞ٠ڢ=ڤ]ڦ}ڨڪڬڮ =۶ힴ=۷۲흾]]C!s,M    " ( 4 9 ( 4 9 * * + ++ ( "? 44" 2 4 ; ; (4 4;(;4%( 4" ;"4;"? W ] ] "F (] 4W(] 4v4 W;-K D F^F ^ uu u,B 2[%B9,B93X9 Wv%B],B],X]3X]"Ff4Wf4Wv3m]:m]S W W 4F" S* W4 W44a a a a (p* v4 a"4a6Ka6VSqfF"vW4WWvFfvWvvfWFvfFvvWfvvvvfvvv ( WB BBXXm4W3m4v%m,m4vmWvWv22p:%,,3::3:3:%,,33:WvvWv̅ "-?" "?4??6K?K?V?K?VD W W4v4[ H?v4HKHVvW--(6(64?4??6(64?4???V?KH?q H?HKHVHKHVWv̓W̱v̱̱̱̱̱̱̱̓̓̓H*\ȰÇ#JHbPz4$,Uɓ(ЅE/"W|Ksdެ9N)6LqgN Lc@@B1 i: 䈝g& Cp!gӁr&4+xJÈ7;* *0B,fAgT^d)0h EL}9_FYsh]hHvn8!hU<VN0]anEĘmݻ+ ӫ_5oO4NXKV<@'6v ` &h $U585F$Ę @rsʱbAa ]f@]ϥHx@)dgyM@'@EʎCFɐ Ti%'TZY%Zn\v&R*qSmP^%|g@yu^?sU\be X18yZ楘fz\7Eh !tQ+вJ:<@-묵q+믹:U{R *WIH!{lt1@y2\s)CYu(2qޤ=fF阨1pI|@{M q?Q1g5yVs`Pm글8 s|;&.XGoF&YP(;{0}r1u_^ٵ]2a%`ap aEcx8k7:(hvgyY:Rjx[7IRwզ>`l5y9usͶ@#!!c4X|wE8t٩59t,O*XLpQ8! z K8Q i[-B$(\ ArNe3Aқw`G;x_ңL"F:򑐌$'IJZbRHD.!~xQq \%v ;$ H҈J b5XUY{Շ[ڲ0x\/TҒ(3gf˼^g| 8 ua)0f1qcfLGpC'5O2bKTDE.Iͥʂӱ# RE\Q>DkZM)ZJIw #VK!QqA֐Ӻ2;5l#eC81j3Nnq;ySp=2*<E@ox[}@kLG`yW_<6ꋋs[:pRکR*#jZxiMVȠ6ϵVRqZ_砀,[ BB\k{@;<~1y-^1V%7 [=#j+wP *k(jZ4YZ'zsH-=0j`׿[1%Ou43ww$I&KwS]ʡ>nۗ[s!+:uxtgzczW~i8bF^d]Uł1^wH9mm8h`ib9GCq^'orQg~ z"8~g+s <tsa#Aa6ǃaW>b?e0&qdYZakmBo(6ԡ~!Xq_2ED6慺pG{u;c0F@Vxt8D\CJNv9TCxb[es¡ŠD=CMakCk?d?!(\'Wx/"X\D4Bl~fjvb1AdbX D.`EuVx!`Q؎X&qo!m؏  y Ǡ CP @`@0* p PTP%|P%^pJjXaTlv #ɑB̐ \𑊡X9ia# )( $j{9S "p$[Th=A>(yTbWKF v%1 p iapd9#` `9[i!)GtQ(w-L]y^V M%|艟(ȠѦa2:iڦњ [`J*j[9zV[R gD*7y!2zT蘊#!ksD38ۑ4$ʕiѪ+:*fʐYЮYk)~Z׺ ɩ5ʆ\@ `?誌 2ⷁQwK ;Ūz˙([^Hډ.@ yS;QU?{t1y*G*P T[i皕[3:9ѺIjl+nkprc:v;O};'v@g@i$/Kozj˧K{D;Y"/ڹvYpzqoJ zڵsY-~ɗ1!Rg͙縻1GCfر%{*c㢉*襆!X°  j" I9 wzڠL ˸1lapP`!d,f,*0 "lLs  ˜ H@~ Ȃǁ<|}lȅLȈȏȋ,sȍȕ ɓɖɜəLɝɗ\ʞl!%S2Gk q_`qK5-PD.$8@],<s&…jtǺܻ̄h$Yּh2D30 @0x>tj1IZŪ) >3`cjNg6E&6.ѦMk0m_nL`oU?W^1]XdM1'lvbcCr (bM-`#5oaPM| oiy[l6m&B6/Fۆuu(|}֑<;yTW>_"G}Gځ&S_~0䓁FQ51F^dTianlxr[Q~6b:95Fqn ^4&#Zh `k2F@dge` #.$@ fRZĐa؛1LYhT_$P@iQ p< Ay0}jU(wuMIfKMj !|Q7cR&6kjWW+;h}ŠFē-ѳ2mOn<-!*`i0}TcMmac  |FP0p Bx<\8M]px~I&,h&Ke\>pwSr+k|lnp2e3$ d(QS (]S,GA'T+b};o׿\cD5 d95Q@7Tڦ πiXLPG5[u yD&3լr_zҷBU*C @wn}~ Wd]98ĩ oevGRAm wJ x Ԥo7Ya4z0{!j]1edw8[d&XhdrքL@:^75sA^K'(d,eWB!,D`x.Ԕ4E-`7 w>% _9ro|Ddgԓ"Izv k|ч @SρdA;Ѕ: PQI@+$!x-FY8/qt+hp4c@!ixKd> Ss1%g IE"@&"SRNu@Ҍ5Un`s8i2@ϭ*|Gv*VջXA /*r|;59`7%Y*nZ֐@xi%mLh-^d[r kµ],e.fvc]@2ͩ(<8)/YCo4MCD*ԋl>Wv.so|*Wv`6 %&<rFw{ 0C !# &g.W|.e3S5ЁnԪ҅ pMȑ"ƤaAZ3;PԧN[XϺַ{`NdBO} "qnե2Vw{ݫ d)Ww7,WVڱ&;Y}1a@&rJG\EH%Ҟl=ԇ]9ZX}bIsx 鑡т~%F7FRS?*pfbc0Vc2v&Hc+Hv-uch/ 1X`/ "Cqワtc75yc|1W0,1A"gtgTgofeERgyg" Sԕ Ƴto2iU7k(e0ᄚSi#HjHv1@֥""r†ptPv[f3%RHz&v7QQH,& 82BhObW|ŇsG}EVd1pwp$xXb88WŧU[8P  "W ^с(s+' -r*c7QLU7Uxn r#aaǃ L@s ٍ9Yyّ "9$Y&yR'wvwSw(bVK /Ix%CA= QyG&VgexF GxqQ+1z9n !E&F5|{~{@!)+Btn{ |ZFW՗vI(!}ԇ?-!GĢRj`{"~"/Q!5`pm!ABd9v,֚"a\N 8(|(B9qQw"R)bCAXn#"`ZHzV$1Y)vX-9#J]aj2f/|/H6<Üm5y#cee c$1.Z) KXqW6Q5)aօ>1APcS6)cAnpyYOz78FucG! *w8Hh"遊m;`-*0/$#n٣%tkjjҊ@c7ԥ1*T`h*J]k*Q^"4$FZ1l?|,x?Ye^x^ $~!h4iisTG sʩ!1=[XHIz@y0JAd9KO[qK?yNN!LZ@LtAO*wHuAIGdu)ZdbMZi} A' ə%!{6q" 0k&!P# %E&S/ \:s 1?Bd4ѳN w 10[n[ E s6 \ !QDa\'YxKYta7㱔-.;40+ A!1T48|?w{I#+ &Xg;Xn ΧkOPj'IX ‘EY)㱯@xHZp1_-I)1 Q4 RlJʎ![N  K'@WB`H#a ¼qv2k`&bK:t+{'BC;s%I&,JI`I qBI \C?i@f 4" 6lq¸e" e 19 ;ٖaI)B `YA Є*kZ -n0d4KRE- q-+["K,ǣpuyH !(IY;c9jQ]l ՠ<EJ 8HLnƧZ\qtAʩ?S ̤ax, z)Ƽy k,.^Q 0aC=1K#lѡaKz\}f ` F!=%Y%M*.҉ Αt #02+ mkRm#VU| w⻉}^4#-4$-|f-M^aM-Ϣ1K-M0,K@jz-}Fm`M5N&PF` ">@ϸp'7-3qraݘTIݍ- ܼfap,{&=˧=ުa?<3{;{  XFq ̤w@n  @q)(0.2-/n357N9;@<E>H~FNLMNT]!U][ sm*IhhbGkӀLCW="MUDW~Rq~s>>1yio&ѮfP; RKU{{"[[Rj>^~ꨞꪾ>^~븞뺾^> ^_7nʾuW.u!u,]    " ( 4 9 ( 4 9 * * + ++ ( "? (9 44" 2 4 ; ; (4 4;(;4%( 4" ;"4;"? W ] ] ] "F (] 4W(] 4v;-K D F^F ^ uu u,B 2[,B93X9 Wv%B],B],X]3X]"Ff4Wf4Wv3m]:m]S W F" S* W4 W44a a a a (p* v4 a"4a-Ka6Ka6VSqfF"vW4WWvFfvWvvfWFvfFvvWfvvvvfvvv ( WBB BBXXm4W3m4v%m%m,m4vmWvWv22p:,%,,3::3:3:%,,33:WvvvWv̅ ""(-?" "?4??6K?K?V6K?K?VD W W4v4[ H?v4HKHVvW--(6(64?4??6(64?4???V?KH?q H?HKHVHKHVWv̓W̱v̱̱̱̱̱̱̱̓H*\ȰÇ#JHbtZA#n=PD'(S\ .L/cάSS'˟? (Gh 1H B" "DB (Qi8w *U :1 8vpӒ=m9|0  L0)6J`)rT)IS3kJϗ;sң7`UQfPHQ'A2P;끼 ar/AxJv0 '0Ed[JHYEɧ"?vRhu( Y*Kh1FQ!T'tyM%@veG)CݩUPwߝ5<"P W!A-ʐ<& 4(L:$RN*IE %^v@ }ї\PE5 ]|QG!D_h~R8A^ b31]s(Vj)D4K,a#![ z =Ћ@*jڊC\e`1sGЯ1l "}@xUԱJ ׸_I(s@]oT$ B.o:*Pë>,u@ }% af21=Imf+.Zٙxݼm8 u(0L7mz^|lA'Ӕ^VV Ufɵ^בf\6"9$ fW$(#T- @3׼hxpQ W).nD7A٘gZZkh9X:頙:vw)u@#u㲛i{kGLܩkPZ}k9j.JGQxZ} 77J&` f:p\%s7b.gZ 2`'I&HA0$u SYL4GT0IJPRؠJ,4ٚiUE)ӎBU+ ą/pɚ_rqH0ܽ7ԀPI{OH2hL6pH:x̣> IBL" "j@V1Ǟd4>%-C. QC% \A@tÕR%GY^4:[hEx^|uQ]2R ʲgnVG.D"Ej|q/K4=u@F Ğ 2=ɗB$M YK/7u"0ihZ&GG##b31oDJ6,I84qG~hɛà=Y."u(]t-8<)N1WU [Uu}pôtHЂ%$v;",tTХ3G)My訄l@+Ox_*h1b"H pJ5Ͱ# !T#,Ff9dT,]rK,WA$DIO6b+)8E3no"H]d@=pB} rVcHLe63u5 rJo Z<a. N RSLZO~StP B@,gGj)(BY@^/E9Nphi}y Ѡ+C؅ [xHB }G& Sס*KTϴou4MnU@Isv{'t@$cm)zg,ß 2R"JS] ]AJ[ -DWֻgOϽw>=!9D S%|d*|w~#",@ރVP wN_!($.0F!t<J;QpKg*ٔPgr'b;EkoA9V~nuV)aT?@Q_$0)1wf{R$8x^xS"ޗ^mWϖSd<^E&x@Au,Xk<[4xGD4Uc'61:nY1z'~"dtT$z4('s?ev!(oUV /yZU%(A!.**o4h7VeBU^RpJ .2l?rYr"DE]%WIn#2D(h (.'3Q+ !-P?xqX[)@hxrcY)ߑ(0D(t3s<(#lK(?nJqa$6Y8%^W\[5xT?ʒ;=h"A(-$<hk`%TPVS똇~@%I~ЈjgSDUu0OD3N'vҊԄ\Εai(|!AӋ/iS`!VRr8 V!{gٗwQD1#h$)GwH~1% 4F~VXɋ[DB1y1FcWJ \Cy9C?<&Dn#JZ&KM<}!ؔEt\KB2#ZRZ.٘IsimYYfߧ#j9Y p ٞMc @, *Y #oa Ei O0Ap #gB qTk_AM" z MO_F@cCQuQ/~ma5 i@ )@ @`=Z٠q pCP9vLi`:XS #ITPyw$zA0YWa"& Ay8W#XWѤ񤧢 pHjK551Ѧ 1R$?xOؔPȣ>:*` Kz ͡pР* u@0KXǺઞC: ʭ ,` ƀɮC\骞 󩞒ja9s])"q# I ᪶{J&6-{"~bZ a(w4A~aY'[J P:Z>[O[JEN [Z=:AMVf[D+I : ѵ @ Kqy { p !C+, A-,{9r Q/@nKqNѳuk{̐^[ѵ_ +p[ ۶ת D K{_+ al T ~|{ ߛQ n{Ke.x3QS8:Eƛdioɕupw$ˁ zضz'ҚTjǡࡱ_#򇢁=]}=]} Ѡ>e^5m ~Q !G,^   " 4 9 9 + ++ ( (9" 4 %( 4" W ] ] "F (] 4W 4v F^F ^ uu u,B ,B93X9,B],X]"Ff4Wf4Wv3m]:m]W F" W4 v4 fF"vW4FfvWvvfWFvfFvvWfvvvvfvvv ( WB BBXXm3m4v%m%m4vm22:,:3:3:%,,33:WvWv̓W v4Wv̓W̱v̱̱̱̱̱̱̓H*\ȰÇ#JHbA ^XhCIIR\ɲ˗0cʜ 3e!\hsgXLh†*tzMA G iOYR V+W|HI`|F)Hy;C^{ .ʬsNv/[֬\M.悦5Eװc;S#e zڠ J%X[HBG<.WёLV:uiP\wӢ+/ @PdX|^=v$DMtgyXg1[eJZk(ۆv8ZS}!^%ԈFORS>CTE GL J:A SXZi5^c\]`a`DSVyL 1iHf6ڙz`bFqzhx>_q9`\hT@cI r]!C*i7keQiZ"DB69fTz)Pc6$peO*R~XW{ F^%/d$s"fBovpB8恩)HW覫nB@u(]n:` U:NTyWGܰmM"0HHO=Z0z/.(֔{mЗe{aX_V%;_&-djC=zƴs봅vu8M=6Ն,tA,% RW U8r7p/~iol2*@ $UFtp|WƧ!fѰ=l*gNP~҃VKٓ>7m-vJimn=vz4G6Րj WkzZH6asޜ%=d 0Cɳjs O2lr&A ⍂[,sNkrGaOzgNE&YޮC*$p3Z2$ixأI9UYT9χٔ?f=sC\vV]5eBxG:q$@můzi!zע'  p Bvèd:8w%Ej;NrĶ*NS78 X zR3د_-&t}˞uK㬖 )H CJ'xL`r.ecb-hO7͞t]3R~.^~T!M AFNTMmv2*RꦥX!TJֲ:(GIY*xt#],ֵ6ovͫ^׾ `KMb:d'KZͬf7z hGKҚMjWֺlgKͭnw pKMr:ЍtKZͮvz xKMz|Kͯ~LN;'L [ΰ7{ GL(NW0gL8αw@L"HN&;PL*[Xβ.{`L2h>,5n~,g.! l,d    " ( 4 9 ( 4 9 * * + ++ ( "? (9 44" 2 4 ; ; (;(;4%( 4" ;"4;"? W ] ] "F (] 4W 4v;-K D F^F ^ uu u,B 2[%B93X9 Wv,B],X]"Ff4Wf4Wv3m]:m]S W F" S* W4 a a a a (p* v4 a"4a-Ka6Ka6VSqfF"vW4WWvFfvWvvfWFvfFvvWfvvvvfvvv ( WBB BBXm4W3m4v%m4vmWvWv22p:,3:3:3:%,,33:WWvWv̅ "-?" "-(?4??6K?K?V?K?VD W W4[ H?v4HKHV--(6(64?4??6(64?4???V?KH?q H?HKHVHKHVWv̓W̱v̱̱̱̱̱̱̓̓H*\ȰÇ#JHŋ3j1 ?vIɓ(S\ɲ˗0c,2̛8sɳϟ@¬ RѣH*]ʴӔDE>JիXjʵׯ`ÊKٳhӪ]˶[$dTp*U o%xa>an?~A+ gg3Z^F`I{>MhgyO*l!5f3@ƚ52ۅ@c(OXs+Y@ = T6X,B( ,$%:!F ߧo߭0U&J5T` 1 u|q>g@¿+{_l@ 6.ăb ](C䫟i{h.6 9X礨 ʂT'@&-$L3K'4>i} !6Jzfdx3Cv8uB4ƣnM$zrx3NtK@ X\ 3fh0; BTE'ɍegb,(K-^؃@ }}l@9P.1 2֐5 h:t cuJBp" 6G ,%yqx|gRQ O'JMHІ:%ZͨF7юz HG% PD_<1KCR, gt$hln 8#8)NMHF6`R1TLiKͪHuٽ6lNE񽸲 Rn#P cS$ǜ$dfJevOUM$W6?|%q2)kزr-5!J$ZeST9ט*Rl̆D$ȸOÖ 4~^3+)AqЄTp=p,Sit:gj 8~Giu@y@`*w^MbNf;ЎRT~$ONTi=lK2L6%= y&5K1UD-=O '98"[䯊xL2NJ$j[ZfȲEVބFwV4_&hB 5¡̹LunvcGX2yPY"FFs%}. WyYjEh0 ő sҳHY-$k7vmJj.h,V9O Q+.tW&]_̄&ao/iE(.Y5 l| q AZYy !li bY( UZdZ=Ii [B1 ~4s n %r"H6_i Uyay9~a sGBL$aItY? IpZٞ'Sٕ?ɔ 6 G) 7 fYF**](:If07y J52Y\! 8sy‡ GAQ:.r. v{I#6v=r i`ўKI :)&DZʧHɨЙQI!8uEEpp 𖄐 aٜ3W3{4?RyF5_S3$yʔIE.ZAZ:ڧ\iꭎj AA}: p*A`ȩ!7s9S@N&.;:^s<]ѤZ"zi4J& ɭ庮ښڮ*/ PjGJ*;6{VJ _CMG֙E;gi3$h``[!^SN] lPXE Gq^4oe@fG@Po9pyW?EQgTDVipqF E]`DNa%\iBJ +`E IٖN\#tPcyJt&’LPᨠP \{:]6-Y DG|~5 ^`](;% R& \@@Bq )!.kp*p(Jr",QV Ѳ1ھKBHo[jiN'+x֕"2h>pr`J}9a ?$hAD;NɆmY J$[_ٻl^=BCi^z%-wseWwww[]vsk$^T lq-Ek^+eFnCTV l DB,HC}XyAwY˾5^Tk(JqFG'|BX: OuU CBV^L\`k*S\臥;/?S?׿% 蜳7+:'H Z NB/puJP.Ƅ!N+Y!:Ô)n@3Γ)T RPp_B\C% F74Z3sRx1hD"# \!Q NlA0"Mrz# =؈i%"VsB6ixUQCYM@.;޺3F XB A0=!AvAJdPkbHF椢D)r5E܌\nUJ,a|؊dl?Ƽ1YJ;rZQh!B6 K0*eVH#Ut u5՞ Z3Z/6,Aq:B>iOxG;xs3T y}4 (K]J!dNyI8N6@!X)͔jLJUr$/p[*A#SbX2ʗ-"hCf DCKZS7ԠO}* L'S/%*U9MHd@\x䪬z_$'"VpLmz֥3e8;tf̖p4ر6!7HE"K=/ Znqy+JDbZ|-plO}T 7'r![Nm_ f9ךmA#:IseyM:ѝvģ:Y7qaubX6f*k䚓:iMcW<s99DxӺ uyBAшUw ArP d ^XoR9dp**[}KZ|,c#Ռq_>_Ļ¥.1B,-7bۭ?Qգ4ZIO{%-!ŅFoSP CP@r*NuΔh=v}|̜ɱ0QQ\ySEi}X'QeKMc+_ h)q6UlUy:<)-הVdX?+),cn 1nyFE;m>xN`C!?"6< i|gW4g{W9Qc|Lbu+u4]495ٲ*=G~[$~(v[6=`T{$} 3aQh]]P7,4 (r45SH"517bh~lrw?x?2D*Y9i`;(!tWrJwXa^9y@1#J"\x&f_B9E7$}9{:q_`sc-:m&QddĊd;aR#Ւ:s]es<ɣ3b\hjvfKvg]g_a=ݓϓ=}f*9etXExi^n9YEw?Ey!ak%Tk'A/aaSGQ ã}k !H(bCSvbm5D8hr^^`|D~F#⅑>-C32~CoD2_Op`Cq^HObqbyQx^@  IrI|av#ax7LM1KG[wheCKRLx5_ioɆ!@Ą Z ~a9=NnNGc!EtOpwzt5G*hFw\,ؒODg!9AQ^Q[s}1yR'iJ1p@)f4קiSsT[zIPT32c?K){t PW|ZEp9+d||}kUm-^@3zES|#~Nyi9T x_aW. GLx%{Ѡa=m0WZjRa84[*ҘbUm0hPF՜K%+H5x ]oC8^UV\u^St5apez'fsąUL'(-IucJ6q6H|)J,IV4n`Vawan4y`f4zhh 2sv769kX7d`r::zjB֤31L;dlXqe(8VbkZfTA+H56]$3(=ygaiٖSm:?c!$b~*i20[6{8:iѳ@ZBR MeX1IQfYIVmV!E_]fqO r:al{V!#ktoxF5GW9# DzіZ{q.k{.[raqrkS``z) DLWS=IkrnѻFK+"`%ȏܒo#qvws[nOn PҸm\\$C2/V仗1y뀀bZyKN2)[zDج4j3t(Y\`f+x@*ڕ* $%𓐸 |ba}J9gHCd ft Z`:qĆkYc1R*xwd $DrjYG).qj+sT, {t3[l5Zc `|ebj`j&3$av9T vC2#UCɚlRb(6f\82KխmŶ ̘<Jɜ,(3b1SWƘeNX[f+L5;ck,m6Ӷ]lȕ"?o (ϋDlϜ>4!Z,J   " ( 4 9 ( 4 9 * + ++ ( "? (9" 2 ; ; (;(%( 4" ;"4;"? ] ] ] "F (] 4W(];-K D F^F ^ uu u,B 2[%B9,B93X9,B],X]"Ff4Wf4Wv3m]:m]S W4 a a a a (p* a-Ka6Ka6VSqfF"vW4FfvWvvfWFvfFvvWfvvvvv (BB BBXXm3mm22p:,%,,3::3:3:%,,33:݅ "" "-(???K?V?K?VD [ HKHV-6(64??6(?4???V?KH?q H?HKHVHKHVg,3 JHŋ3jȱG/Y0I#KIҤ-?AMZڸi.*U( `S`Ѣ)Ń'DZ:QjI&FeZPW LaD )0b ƃSz!pE;{,Ae @%ŀVdW ,`a10E, Y\[K # q(`kSKN[hqv޷+]C ܬQ}.F)c<Ƭhukq&Vay Fg rZ<(]y1Xa߆vx (b "&Xq94%dԇiJmi1dSؔnU-YeTialh5h{WʁI"CMA)}4!Pw湧xzg2:cTEhP46pp6'VjatapZXb1PI[WhrZ  n쳂ħ@ImIjQ-S*1^$.>a)nzo4:U v (ZZqѕpGjM+Wъ#bbbiSz6WtVzI:pF"9C$kYkGmڔZ96ySKWwZ`ѝ'gZl-󿁑3y@oԫ!0"bnNʗ|7rO%%L RyF'x:I%JNu>mBIXn |+L0-V2.̠7z GHiI]&,o'LTԐ1H6F OHC{@8+@B"vQ_-/ѡH[Tqm/}7d1ԱdF5#S8C /)D|IXH`\D8aiФ2E/f s$BFOa@4Ia#Q8% 4:J@Z;/^8]4悴B𠕟:6L(YȒi*8b$M"nT6t3U²% s&C O>)zQ c$)甑-s$+NHF:+ TbF I%.3byk?k [-])̰[g)E6rmJ-bpI"I]gQIrڜ) +ǦՁG)0鵟N|C>l) ո튩Jg$Ǹ iՒKjYgF6Rfjm|ģa'k]MAfPlj`Ż V st0n84 TPLpQk`yFB"ӂ~HZ]K@zDzA<{m@F6"'К/`)_e}+Rk:'/F K wЪ7A O 1A~X(NW0XaXhX"Ktc : 6}!G.r* ɒS'N^WB?2AXɊ6Ǔ!$'˝&NcI*!eM%!isMR]FA~P؎ɰLn=y PC|" #hZ aӴ|!ݐm?O>\7DEuiSC:a'4nUbXRG / EX6#@nl>S iRjLTQ2%8ڣE" (D2rS?g7Y uLiRn׆PmTb#r$S-6iX6ȲTkuiLM0bAl~.7 %EUL%9xS4}!\4sf0$DpmZop׍<!oNG]>1@8Us`aٟ ď"Zd(h ڠgfYyH84tEq2dapZ*&wX%|Fqԛ":iJS\vH>,Tmsx gg!JHk*4#FXW7Oʎ\9ãەXʕi#:l79yV%I 6qvYW)Ėpj9d<#yU`ҧ3Z9 QF !g,N    " ( 4 9 ( 4 9 * + ++ ( "? (9" 2 4 ; ;(%( 4" ;"4;"? W ] ] ] "F (] 4W(] 4v;-K D F^F ^ uu u,B 2[%B9,B93X9%B],B],X]3X]"Ff4Wf4Wv3m]:m]S W F" W4 a a p* v4 a"4a-Ka6Ka6VSqfF"vW4FfvWvvfWFvfFvvWfvvvvfvvv ( WBB BBXXm3m4v%m,m4vm22p:,%,,3::3:3:%,,33:WvWv̅ ""(" "-(6K?K?V6K?K?VD W [ v4HKHV--(6(646(?4???KH?q H?HKHVHKHVWv̓W̱v̱̱̱̱̱̱̓*\ȰÇ#JHŋ3jȱǏ A )`$8L9H~tȣJ*AbGO*xSKZ0R;=84jO s T=t l:eٔgI0ö-3\}1@smy)6%wff&^%bM.|XȆC&lLC [é[WɁq#,QXղ //lQڜyzgpA=_p9tdtZ;MwtEY]^-H^ݵ=vgh6g|ݬbZ\DnM ~S+cF}y}wR)$e.x魷B^,q.v#X3y+ÓSɅ@4rLJrAydk^4FzN4LG2e 5L )u\#}*K(g91eeviKl6 L\Z'uF~3N]ҔE *Ќڵg&\)K8|ɋ^0^FYo+&nx4KG)ߑ&uC2 "cE2qrSPYlg .LIjKg:`)i[7ۍuZ~:DIS,O{x)6Qzt$54$0N B,K+=:UWHQ.fKR'2J#1R%5 e{chG/N͂ccp1i#EsIl44HbSEPҖ a%L$˙8MmҜ)PE7%Mkr >uU VKPͪVÅխz` XJֲhMZֶpk 񉺞8 !Jd`qu HqlaX@H zC"m|pd$ I|1Xyّ "9$Y&H dpqwzz'YY|(&/G>EIk1VDPG-( &g}598tC|VZ$m$巐J ƌqW%8>>SsfZI?'g`!!y[%vcw96sPGĨpVKD؆mAu5 5WPWKqAc&)MNpV+g:w?-_1wTH[_옌g )YCK!XVF '㖅ex]9B8xq6FoHY=:fV r (-+p3qq6'}T9gbC=mek GEND-%Ep[7Xlt9r+8ۉVwPH 9 XWHo+%{@{4Oi3`X%|nmKBP`b:dZfzhjlڦnjxY[蓮Gz~f@;xHYKq7C h1|q|f꒟ { x Agi/ЎpɧAڠhlZCMQ-8Bt(g tAX~iZeBI@ G.!Wz^Y`)46y1#x؈'2&A9dsZxz: R)l>}!%29r*FZq3+6a+3G!08A.fchV,Jdnw(qg 0>؃gK;eOzZґS1'ٸ!/2-!D'Hⲅ]xn@g 5w6h9ٳ 7YRvi9lQ+mSm D~X8wp8;a +&C2W9tk.~0K~-;(kV:IxdҊ*5f)3-36c "瓯`'bKIrCD*1K)pQtx @;RJU頸4&.}C‹"S DFXU:A6ps%rU$tٚ:" J-E$g蛾1H0M h1)*T[DLHwBF5+~sst[ pPW@z !u qU7uJVI_OMAN)1NN@*4#QOUpOB^n'ʲd@G~f]7G,m~ih Uwfw<&Ȅ|p5g!8,M    ( 9 ( 4 * + ++ "?2 ; %( ;"4;"? ] (];-K D F^F ^ uu u,B 2[%B93X9,X]3m]:m]S a p* a"4a6Ka6VSq (B BXm3mm22p:%,:3::%,,3:݅ "6K?V?VD [ HV-6(64??6(?4?Vq H?HKHVHKHVqH*\ȰÇ#JHH> Xɓ(ST(K _Pl2L0e\ CH"& Y"HhT`BwJp 78`$jVWmB۷pʝK@)$qұ -YFL0JUpXKƉv`h㐁;ʀʔDŽГޠv C7aj8>@:, N!XI!"?N]jLp4q=W7Z@81ӪOHOxh {#7& b w=5h3yL!~Ev @v" }t^~/p}šAd#8j>Q&!F !LVY\K\z]b( ?X(_@D=``}}EЁ7Y i壐ZZhArQ馅@f jLb(K ѩ!hwQ0q6d}_qP }UhVUF+-ua0GSFܖ8^j窷"jĒC5"(ށ&odžl8NGa SԘd?c\(g]l껭p9`V4*"+(D ,sIP& <#wM]YȞGWn{Ո%>Nݴ v,v-hA@Z`:dCAͥ HS| U%0R8PjPU6PX3nlGx`b oNۨꬷ馻.nذ~~:o'7G/Wogw/o觯/o HL:'H Z̠7z GH(L W0 gH8̡w@ H"HL&:PH*ZX̢.z` H2hL6pH:x̣> IB򐈤VH%Ktp)J*dq!?,D   ( 9 4 9 * + ++ ( "? (92 ; ;(%( ;"4 ] ] ] (];-K D F^F ^ uu u,B 2[%B93X9,X]3m]:m]S a a a (p* a6Ka6VSq (BB BBXm3m%mm22p:,%,3:3:3:%,,33:݅ " "-(???K?V?KD [ HKHV-64??6(?4???V?KH?q H?HKHVHKHVH*\ȰÇ#JHŋ3jȱǏ CI(O\ɲ˗0cʜI͛8Lϟ@ Jј;Q]ʴӧPJ}TԫXjʵׯ`ÊKٳhӪ]˶۷ROR򣋀(!P.L h4@ޓ7TP}/42+myA >1|2mۄcA͔!٫(W+(̏?449!0B(J6`gTG,KAq0pmV4@1A @ 2v 2P \uQ(bD5*5h6Bd |w6 aKVe 5o\e/W$VDB Bj2T|Ş}9#\ A3h&PCT$pV%46PjACyI7Ekт@%dP - @"lr %I 8 O@a IA`*?aB'qxqK5%` |2DipBЬ@_LJF~A !@=K  onIU*p= Jvk4t uFh*(\?97P PgR6k;u JBZ54I ޫ |x4.N["!؊h'Ei0[js*,@މaA~U%~c힣[πMBЈNF-tMFJ:ҕt/ iM{G À!a,;   " ( 4 9 ( 4 9 * + ++ ( "? (9" 2 4 ; %( 4" ;"4 W ] ] ] "F (] 4W(] 4v;-K D F^F ^ uu u,B 2[%B9,B93X9,B],X]"Ff4Wf4Wv3m]:m]S W F" W4 a a a (p* v4 a-Ka6Ka6VSqfF"vW4FfvWvvfWFvfFvvWfvvvvfvvv ( WBB BBXXm3m4v%m4vm22p:,%,,3::3:3:%,,33:WvWv̅ ""(" -(??6K?K?V6K?VD W [ v4HKHV--(6(64??6(?4???V?KH?q H?HKHVHKHVWv̓W̱v̱̱̱̱̱̱̓eԅ*\ȰÇ#JHŋ3jȱǏ A )$L92C)ހ С3@)pv( @f( PFW2uJgԧ:q1KϨ+ƔZ4 0Zb@+ֲpk#5+5 '4=7,Ǿ* f,ٽ /;rE ,3bGEj~e3IBaG_aȵD;DV.kBmYqF[1KRdi.(BDr8(q)ʭr+,Er<)rCYĉ_Jg11!]0A@_"jEѠK^EF+nyQF#(+DY d=6` |aGOAA0$;ĜY6Es)OG%Ng<`v.vJ%(tjם =2OcT DpH/glX51_,ի* "JERQ.ܥ9f^eYIny}H[`B\\KWu'MB jf69hiBr $dDA0Z ll%vH 2,Y@RUVէ0`sCWYۮ"IB;#-"m@!YL̢&ucS8:c=e!C Uef#)LAt33m6Yj>]nE a\| a^L%ߗwX<[עg*߄%],/VY: X\4}l:/ BZ5ш13$D&ڙazf)U7 ѡ@`SP ː3Xxyв]O;񐏼'O[ϼ7{=Uߪ0̖$C:ޕEa X+(O蠦*AE]y€_I~E]/a oL@lmnu[k Orm!7 :ЫNA{ŦKoQ}F~VC&'y+g` H~Juse1"d!Uv1 ^F:N"ht;sM'b{[ypVh!ԦyFc3c%b$ke=Uh#b*C:TVW9_fcVTfyqfgt&gwvh|LKAB]E>xOiv'f('DOB>AjabRjAj:k\r !hcdU2$+3?Hv Q|W^+m(!+o%0uS} u5 Fc83rʹֵUIhqH XiU.3]~ɥpcsAZn9e>Q2aFoArVV3%p/'`V-akQ#]UIK/ QIh"Q7uU,[yb_dGfnwڴUYIUOK+|W8:<ٓ>@B9DYFyHJSCPz A  %UWX '{[K)v|aӃd)#Ya@YCZWZa 1|}ЦlEW,j oa%pu~{$$ wō Sx}wWO^ޱ^*U0W_tYTP"BřagtEq&(Ri T88V%98RB9TE,Tf )Q=#ae57f^ m e`&f_usyg]YУBOf&2bIt!JS 0%6 y([Ek(*Wvr.T6Zh9hiao p \jJUʅpiNM33 S At$XaY3衞3a_g@C`]((xXULwH?|Gi w5VGQu)S)Ún7{/dYp6і뱗1|I +71! D,*   ( 9 ( 4 9 * + ++ ( "?2 ; ; (;(%( ;"4 ] ] (](];-K D F^F ^ uu u,B 2[,B93X9%B],X]3X]3m]:m]S a a a a (p* a-Ka6Ka6VSq (B BXXm3m%m,mm22p:,,::3:3:%,,3:݅ ""(" "??6K?K?V6K?K?VD [ HKHV--(6(64??6(?4???V?KH?q H?HKHVHKHVBș P 2 JHċ3jȱǏ CI;:qaJ%cʜGA.pd z` ZEΙ$ Q< y`!DBp dȐ .:^%hC\08A ҿ "'%4%)I ;8Q#<'?!s!d!#6ᕪen| B@!ZFjVx5sEa8/DqysUk]p"a0&=1D)Pt8`\=zhEVX" mD8^c0pRSmW V\Um@T(ah≈a"(TY IFeb-M'&US%dXvVJ 9ؑrEv˵Aknbp ƀ3PYqΔS$AeD6L AJ.YCti @aq FYd֙if:Kz,$_ ?TP5َ(J;Rpip>VqIv[qvDs =ˆш [-GxntLB#D ŤD<n^ܔ b Z/Ҋ]t+c-vX*g<D7 @r* 1[D b)U:f\oѐ^| pk\Xx]c !`FZDvhMrCIDYIT%dUGaBeg`|MkPd(F>EF-: 1^FuQX6Sa5c(\n'7G/Wogw/.C 9fb/JDSe2^0f/[ Ѧp|gf=2t|b#2=¥{p4ldșMb4(&H~312 ˔^lpxpt8oB 6D#8>6#y|"kDA^Bj 8h`ѫbD(7N>US !9,=   ( 9 ( 4 * + ++ "? (92 ; %( ;"4;"? ] (];-K D F^F ^ uu u,B 2[%B93X9,X]3m]:m]S a p* a6VSq (BB BBXXm3mm22p:%,:3:3:%,,3:݅ "" ?V?VD [ HKHV-6(??6(?4?Vq H?HKHVHVsH*\ȰÇ#JHbA)@"p%CIɁ"D+[TɁ3tAD1LB9 è#; |a8p|@P с ncدNKݻxCXB%GQ$+J?V1n "1B"$BqDȄeDmcXnTe6G pkXxjvqУKXA$3x9M$˟Ooq9l J b_[h=viy5 AUm6r yeY Xedv9|Qf+PQa(h_,"2xW F0C0X %|nmWe $Ɩ9o =ITl f V2d9FRH @׍8vyg-Ѕ pф@v'wg]0(eTI @E %'o7GPBεVdAq\ ATeပ0jX)무&GV`fKWM} DƶT]q2QXb:.1bieW :Г !|X{E4X0u`+`:X<@ #|Uf%nbRw/Oޛ>wo= HL:'H Z̠7z GH(L W0 gH8̡w@ H"HL&:PH*ZX̢.z` H2hL6pH:x̣> IBL"F:򑐌$'IJZ4#LN:pi( Q<*!<,4   ( 9 ( 4 * + ++ "? (92 ; %( ;"4 ] (];-K D F^F ^ uu u,B 2[%B93X9,X]3m]:m]S a a p* a-Ka6Ka6VSq (BB BBXXm3m%mm22p:%,3:3:3:%,,33:݅ ""(" -(6K?K?V6K?VD [ HKHV--(6(646(?4???KH?q H?HKHVHKHVyH*\ȰÇ#JHŋ3jȱǏ CRɓ&I\ɲ˗0cʜI͛Qɳϟ@ J(M'*]ʴӧPBERիXjʵׯ`ÊKٳhӪ]˶[# 5M nN7L QdBJH #fxŀ a tii `&9AA4FF'v2 I`]j`MVF&}qN!"*I2TUfz'mAƉiP(".D&DR qq$poq)E gf"wv.㲹))fV&š_$&]:335 o,oQ RPz+!ˌJeȇyP & !̦uDA)%`xS5[-b a8zA Q(I )^a450m]3*@$8G%0HN"iNr^0h\I@3t() ) RH泩-i*0ا̤i 0XBCH;r & M!@X.gE4<ԋ[t"AH!60Y,*-%R E k DҜtKefHͬ33̋Z{|E7%oIlR$xkJ4sg>$ pL=,OXRc#^BVCwPW99t(ѽB@4 ] R& -Wàռ| <RDj%%6`mi?l =ͫ^׾ `KMb:d'KZͬf7z hGKҚMjWډᵰ}CkgK[ͭnyplK\!c,+    " ( 4 9 ( 4 9 * + ++ ( "? (9" 2 4 ; ;(%( 4" ;"4;"? W ] ] "F (] 4W 4v;-K D F^F ^ uu u,B 2[3X9%B],B],X]3X]"Ff4Wf4Wv3m]:m]S W F" W4 a a p* v4 a"4a-Ka6Ka6VSqfF"vW4FfvWvvfWFvfFvvWfvvvvfvvv ( WBB BBXXm3m4v%m,m4vm22p:,::3:3:%,,3:WvWv̅ "(" "-(6K?V6K?K?VD W [ v4HKHV--(646(?4???KH?q H?HKHVHKHVWv̓W̱v̱̱̱̱̱̱̓ȅ*\ȰÇ#JHŋ3jȱǏ 1 )$4LR:H:% ٙÀJov)cН/ct'1 H0O,RiW)#aװXbʴk۪D&۳2E<,Xnݺï =RecCxxbxɃ'|SL$ Gx㧙 bjV&׋gOѦuUVl0]/MS^ A-2S! ҩ[0 \[ĕ/~saӌʜOGRufD_}fgőWC8ƟY` mՇY\]YaT%qX_@,0VY*%wc57%Bg"bDf.6ؑ(YyؔWE$I)xbKsyA )f)1A#hRqcv֧)Ip. o`zՄHܰWLX0`'%R>\؀V9eXK`ЦAJK0uDgD{gT4Q.V!fAH0pzj¡4dct'W}F&X 3,nV ` ߀Iށʶ7(;G7E&s+DYd>-kak#b|%N&L_Jb}Ā) V|RO Re1;YVhEp0'lLҒDvo2L no|<<Ciu`Ӝĭ(MWƳ Dv hNHe'[pW׬ըaPb]ơ$0 (3zQ9mث)93 }اHK.g(w:vu&Zhu#0$p^ mH_ҒյOa cb܍_li[欅F>jE `bC3c6Y/֋<Ë%B_ #p @`^&|bt-,6"+i@#j9s? @*#Cܨ Ӡ?,U1$Sg䵌?dCWa 1K?9=4ܫ 2jƋ*}cofP*'ާ+@^Ida'U5a52qw Q6ceP`e1Afu63(GdhkAu{pgxf_ G's@_iwPh%|-#;MQc7fC^52fFXv`w 'Oy`¦a4Q F9\>f%nd*ҕ&f 1%|x&ypfE‰@Qve!]4)P#|ml:BMXFUA(7 ߦ8 I5~hnWnvn JTcҨUJe4LowfA!C,   ( 9 ( 4 9 * + ++ ( "?2 ; ; (%( ;"4;"? ] ] (];-K D F^F ^ uu u,B 2[%B93X9%B],X]3X]3m]:m]S a a a p* a-Ka6Ka6VSq (B BBXXm3m%m,mm22p:%,3::3:3:%,,33:݅ "" 6K?K?V?VD [ HKHV-6(??6(?4?VH?q H?HKHVHKHVf l؄0@lJHŋ3jȱG%H#K~\!W0*TdRB!!p@ !(UDB01C #p XݻxX@OX@E 0vlذaf5*Ð l9SB> iU*VU d K+>#qQ- e!˛?OnF 4q01! P+ ĈF֥hiUoHM{>j\1ށ&8P p4` :`s ~dvC\xOVQQf!:5D(Q=p1-$)` 4AQAB yz1F @NB)%u'9\g QDGTF Q!@|&hBHUghxՋ1ĝDXEBe #By l֙VI-T)ZzP$v_9fZSk))!~#}ުU`u[(%q6kiB(^Y aQA*: @%ֺ ԋ?Zo`p,OCJ s~ Cݝ @w  z>߫)iEXnP %1n%45)0'<0 B&Yc1zfuhAtFFSH 4F lae@36t@kڕEC 71&mSL3뒥 Rd aucmieew砇.褗n騧ꬷ.n{`$CYؙ^J@=4CLD DETE_%BPOe5Ddu_/qF cCH6eA6( eZubP$F U'f54!Mfck"uWv 6 laR r'xK?4M]ïGM<1@ȏփh?o[Z0m-A<2YB""g,   ( 9 ( 4 9 * + ++ ( "?2 ; ;(%( ;"4;"? ] ] ] (];-K D F^F ^ uu u,B 2[%B93X9,X]3m]:m]S a a p* a6Ka6VSq (B BBXm3m%mm22p:,%,3:3:3:%,,33:݅ "" "6K?V?K?VD [ HKHV-6(64??6(?4???V?Kq H?HKHVHKHV}H *\ȰÇ#JHŁ&H;^(d'%XPȗ0'D1bN;I؀iD8!*`&ĜJիX(]5fmX JBNd"ʥ@g͛>>hP< T &N>] 3k|1,> Z4gh`$+@RmDPZr=A'u+ǃУKmY_3!0ڇ[xov஀6;&X0a7χ|>[t1ށ&HlXĠvz G x* H(h~S`wP>N{W|7֓%$@Wf`Hn  d@7$Qk,Mt$L(\}wP!܄XrySQ4Py>pTcgLmvqv×>@Yn^7ןɉ|@]z)fԝEzC`yWޖM38h~ȉު sJާ"[ @h*RKګ]kV!@UKW_>,aJV@)_D 0pᗡyJhjMpB4 S"cdخ]Bqڃv_0,4l8<@-DmH' 3BRHjpAJ#Ғiĵˮ+ @e?$@OHjfxp>^SOW@4o;C91ŀ A%UYOתXrYg}L,Zc0DxF J,p];zIFPFf SxIhqqUvYmr5gœj.dzmvS b{xion%tiqOM1!SQ|<pz vX(}'<)yð])dFrf:p0a ] TCAx!H@ O3IA>|م>ԭ ?Bj" .\ӁTvcg39!xBÅ@1S\I*#g`O d*#PzR^&%*-Fļ\4hE&n*B"Ł!t2 {s)Z 84юaEPRTIDB a=H(LyE/'Ar0 B¬' bE=jm7@D@@Sf4p>@NMp8IJjU![ 0 !b,   " ( 4 9 ( 4 9 * + ++ ( "?" 2 4 ; ;(%( 4" W ] ] ] "F (] 4W(] 4v;-K D F^F ^ uu u,B 2[,B93X9%B],B],X]3X]"Ff4Wf4Wv3m]:m]S W F" W4 a a a (p* v4 a-Ka6Ka6VSqfF"vW4FfvWvvfWFvfFvvWfvvvvfvvv ( WB BBXXm3m4v%m%m,m4vm22p:,%,,3::3:3:%,,33:WvWv̅ "(" "??6K?K?V6K?K?VD W [ v4HV--(64??6(?4???V?Kq H?HKHVHKHVWv̓W̱v̱̱̱̱̱̱̓%*\ȰaC+d 8Hŋ3jȱF =II Az1(&eJ.CLˆ7@ycB@N 8SR\4iL1 HQ5֠%UgoD:mKa"]+0aV `KH^mv0]Bn3ٗ֔9:wٕ9ef!*$@wi a皧6QB(ƤȡiDFuN*)%A _:e6xqyw^~S~^ecrf z<,ؘU4bAU{-֤g|kmܞ &NE'FXkH٨F+ 0'*G+VJ*I!H C: a yzABJkfY"߱lެ3٬%Z>**;dE A)X(iR"DK#2uKZ>UqWbDЍ(QY5] Q/lp sA,1 Px,] y?ɁYxkXA#[FK/s韰 &C!D tފKT,5a å e*H1BJ|OZ5ވ) ߩJ r^{tgN%c:l.h>kZ?Z ꥱCKs$/uwwɋyjI@(+W󰈅o-C8 Ud#|O M(Cʹ,}2;g} QZw-pm[X]9NCQQoB\;Jq㶟4c ~ܓ±( H= В%#}=}8[vd 9+9N2$5m ӷ.$:.\ Yr;D.yWًڀl8D e/iE.xKBlVٌ0D0dJG Ϧv kwО=%(]f)Bzt2T#4>JъZ zq(H L(= A+$0e("Y+Nw*PN@ PJԢHMRԦ:PN@?``PTNJt%bVU YJSR4$,uQ(:$ .ُ2L$ExGeDW DK!d -E0 1U 5Y FZ&LM4H6Ao`HoL"A@"* P-ALzhE1B Cwޤn01 ]7̩GVIn{pqaH)2 #Z٬zPf"mw.'|OXE*s(7}_ARŢێtUeXi#a% !綄'" L`?郝M(@|$+^|5DFuX ؍x!#SA }b1$*F@Wc@%@U"HvȀN,A:gK&""6Q!@@i r7 )ds&e#ssK sv k@?1h1xkC^4c %[ }A2!;@mԞ6R_4AˎB"- p1̼͊@a&OΉ h!*Cy}%͉q! 8,   ( 9 ( 4 9 * + ++ ( "?2 ; %( ;"4;"? ] ] (];-K D F^F ^ uu u,B 2[3X9,X]3m]:m]S a p* a-Ka6VSq (B BXm3mm22p:,:3:3%,3:݅ "" ?K?V?VD [ HV-6(??6(?4?Vq H?HKHVHKHVqd`$X80@ 2tHŋ3jȱǏ ?"aCG< A 3d S!#, )p38F6n1 #DP  Ԋ(8hRH 5\x)8T d_vPThձM!K>9z tр"U6v D [Mpȓ+_1@%8p]:9h r~; pzK@X@7:_ `moǕYwmUVބV \dj!tЃtbՀSxފ+% hj᧟>:n0$XT@\MX^ɀ+m <` Ԡ)&fYj UY(T } `8g7 BXH%c\ax)i|*!(C{j'ed'x'uF[~84lv8xQLUDIԡz!'P['\ш;!l3JZ\MaGfE `^{ ɋÜDw# GZʄhmqD(PPuu07'u u8"DM yTzVԙduC Lg)ր< | > ι[U Xz`e!7(e:dg)?wgP_:ɲ) B򂐷 !85 V ,֖ K,E+Ŗ5:xqӅ26$™ #k6y`a 4@%ēa|̐Ċ4pG 81!`|0(v& Ϭ0mMKB2 {^WDìCse=e8bNƗb`-vg7i KYk-'EDECDtCQf&QZ Er-.]ahգCh^B``Z L|:ie~ADi= Bel a.vYQf|qRUAP! A'c2Aġg5(1K5Мh$P@)~TH /,)N*IjTyo*җ;JfIXEhVbx iL`Ŷ1y~[HFt@6ND@ pZURpda鰢@ U+)X.:-,Uq $]AqzrHe:++ )۫.sf,<#@ $4< D eR`H4W E5 hCL QMŵ1B ; ~tޑkqZC*DBPdy8gcf?#Ε'h͞kY72C44G9o͒pUOUYxwH"i2I=$'Wna\2IrsE9'XY~ `~:AӭzӲc ,dmkqh459I֒$z}kIa'0cX <ÎJHf|RЇhI ~"j.ǚ݌?4XĭҐWVk+ZZⵤ^#H%]”~yTFYJc$h9^8p=vL%+)o13(J;$&E#%h(uܚvpES Pg n~Yr`#Dϛf.G pu ½\!cIvȥvJY5I V"ؙqf_4MhBԩ ]DWIt(DQxVT Fpњ$'"M)FCEas.#Jg:S!>, E$  4 3 3 4 W 4W 4vT T s s W W4 v4 W4v4v       22WvWv̓W W4v4Wv̓W̱v̱̱̱̱̱̱̱̓`*\ȰÅ>HĈ-jXcA CIɓ A,]Z`J*k")Nir'Hagτ@5Y!< iЉSOEiXQNDp% :$as0˒Z7L-mW_-anō=<$S8p 'N5Ks,j^&|0 ,K z׿Yp-85nk#5Z4O t '$JփB.r[os.|#x,Gů:\fv7h& 6MJ8aIP \HX!I,0Ih'HX"A8jD0=ZP L6d:䤓PДMIO!>,*  H;go-pretty-6.2.4/progress/indicator.go000066400000000000000000000145621407250454200176370ustar00rootroot00000000000000package progress import ( "strings" "time" "github.com/jedib0t/go-pretty/v6/text" ) // IndeterminateIndicator defines the structure for the indicator to indicate // indeterminate progress. Ex.: {0, <=>} type IndeterminateIndicator struct { Position int Text string } // IndeterminateIndicatorGenerator is a function that takes the maximum length // of the progress bar and returns an IndeterminateIndicator telling the // indicator string, and the location of the same in the progress bar. // // Technically, this could generate and return the entire progress bar string to // override the full display of the same - this is done by the Dominoes and // Pac-Man examples below. type IndeterminateIndicatorGenerator func(maxLen int) IndeterminateIndicator // IndeterminateIndicatorDominoes simulates a bunch of dominoes falling back and // forth. func IndeterminateIndicatorDominoes(duration time.Duration) IndeterminateIndicatorGenerator { return timedIndeterminateIndicatorGenerator(indeterminateIndicatorDominoes(), duration) } // IndeterminateIndicatorMovingBackAndForth incrementally moves from the left to // right and back for each specified duration. If duration is 0, then every // single invocation moves the indicator. func IndeterminateIndicatorMovingBackAndForth(indicator string, duration time.Duration) IndeterminateIndicatorGenerator { return timedIndeterminateIndicatorGenerator(indeterminateIndicatorMovingBackAndForth(indicator), duration) } // IndeterminateIndicatorMovingLeftToRight incrementally moves from the left to // right and starts from left again for each specified duration. If duration is // 0, then every single invocation moves the indicator. func IndeterminateIndicatorMovingLeftToRight(indicator string, duration time.Duration) IndeterminateIndicatorGenerator { return timedIndeterminateIndicatorGenerator(indeterminateIndicatorMovingLeftToRight(indicator), duration) } // IndeterminateIndicatorMovingRightToLeft incrementally moves from the right to // left and starts from right again for each specified duration. If duration is // 0, then every single invocation moves the indicator. func IndeterminateIndicatorMovingRightToLeft(indicator string, duration time.Duration) IndeterminateIndicatorGenerator { return timedIndeterminateIndicatorGenerator(indeterminateIndicatorMovingRightToLeft(indicator), duration) } // IndeterminateIndicatorPacMan simulates a Pac-Man character chomping through // the progress bar back and forth. func IndeterminateIndicatorPacMan(duration time.Duration) IndeterminateIndicatorGenerator { return timedIndeterminateIndicatorGenerator(indeterminateIndicatorPacMan(), duration) } func indeterminateIndicatorDominoes() IndeterminateIndicatorGenerator { direction := 1 // positive == left to right; negative == right to left nextPosition := 0 out := strings.Builder{} generateIndicator := func(currentPosition int, maxLen int) string { out.Reset() out.WriteString(strings.Repeat("/", currentPosition)) out.WriteString(strings.Repeat("\\", maxLen-currentPosition)) return out.String() } return func(maxLen int) IndeterminateIndicator { currentPosition := nextPosition if currentPosition == 0 { direction = 1 } else if currentPosition == maxLen { direction = -1 } nextPosition += direction return IndeterminateIndicator{ Position: 0, Text: generateIndicator(currentPosition, maxLen), } } } func indeterminateIndicatorMovingBackAndForth(indicator string) IndeterminateIndicatorGenerator { direction := 1 // positive == left to right; negative == right to left nextPosition := 0 return func(maxLen int) IndeterminateIndicator { currentPosition := nextPosition if currentPosition == 0 { direction = 1 } else if currentPosition+text.RuneCount(indicator) == maxLen { direction = -1 } nextPosition += direction return IndeterminateIndicator{ Position: currentPosition, Text: indicator, } } } func indeterminateIndicatorMovingLeftToRight(indicator string) IndeterminateIndicatorGenerator { nextPosition := 0 return func(maxLen int) IndeterminateIndicator { currentPosition := nextPosition nextPosition++ if nextPosition+text.RuneCount(indicator) > maxLen { nextPosition = 0 } return IndeterminateIndicator{ Position: currentPosition, Text: indicator, } } } func indeterminateIndicatorMovingRightToLeft(indicator string) IndeterminateIndicatorGenerator { nextPosition := -1 return func(maxLen int) IndeterminateIndicator { if nextPosition == -1 { nextPosition = maxLen - text.RuneCount(indicator) } currentPosition := nextPosition nextPosition-- return IndeterminateIndicator{ Position: currentPosition, Text: indicator, } } } func indeterminateIndicatorPacMan() IndeterminateIndicatorGenerator { pacManMovingRight, pacManMovingLeft := "ᗧ", "ᗤ" direction := 1 // positive == left to right; negative == right to left indicator := pacManMovingRight nextPosition := 0 out := strings.Builder{} generateIndicator := func(currentPosition int, maxLen int) string { out.Reset() if currentPosition > 0 { out.WriteString(strings.Repeat(" ", currentPosition)) } out.WriteString(indicator) out.WriteString(strings.Repeat(" ", maxLen-currentPosition-1)) return out.String() } return func(maxLen int) IndeterminateIndicator { currentPosition := nextPosition currentText := generateIndicator(currentPosition, maxLen) if currentPosition == 0 { direction = 1 indicator = pacManMovingRight } else if currentPosition+text.RuneCount(indicator) == maxLen { direction = -1 indicator = pacManMovingLeft } nextPosition += direction return IndeterminateIndicator{ Position: 0, Text: currentText, } } } // timedIndeterminateIndicatorGenerator ticks based on the given duration. If // duration is 0, it ticks for every invocation. func timedIndeterminateIndicatorGenerator(indicatorGenerator IndeterminateIndicatorGenerator, duration time.Duration) IndeterminateIndicatorGenerator { var indeterminateIndicator *IndeterminateIndicator lastRenderTime := time.Now() return func(maxLen int) IndeterminateIndicator { currRenderTime := time.Now() if indeterminateIndicator == nil || duration == 0 || currRenderTime.Sub(lastRenderTime) > duration { tmpIndeterminateIndicator := indicatorGenerator(maxLen) indeterminateIndicator = &tmpIndeterminateIndicator lastRenderTime = currRenderTime } return *indeterminateIndicator } } go-pretty-6.2.4/progress/indicator_test.go000066400000000000000000000166731407250454200207030ustar00rootroot00000000000000package progress import ( "fmt" "strings" "testing" "time" "github.com/stretchr/testify/assert" ) func TestIndeterminateIndicatorDominoes(t *testing.T) { maxLen := 10 expectedTexts := []string{ `\\\\\\\\\\`, `/\\\\\\\\\`, `//\\\\\\\\`, `///\\\\\\\`, `////\\\\\\`, `/////\\\\\`, `//////\\\\`, `///////\\\`, `////////\\`, `/////////\`, `//////////`, `/////////\`, `////////\\`, `///////\\\`, `//////\\\\`, `/////\\\\\`, `////\\\\\\`, `///\\\\\\\`, `//\\\\\\\\`, `/\\\\\\\\\`, `\\\\\\\\\\`, `/\\\\\\\\\`, `//\\\\\\\\`, `///\\\\\\\`, `////\\\\\\`, `/////\\\\\`, `//////\\\\`, `///////\\\`, `////////\\`, `/////////\`, } out := strings.Builder{} f := IndeterminateIndicatorDominoes(time.Millisecond * 10) for idx, expectedText := range expectedTexts { actual := f(maxLen) assert.Equal(t, 0, actual.Position, fmt.Sprintf("expectedTexts[%d]", idx)) assert.Equal(t, expectedText, actual.Text, fmt.Sprintf("expectedTexts[%d]", idx)) out.WriteString(fmt.Sprintf("`%v`,\n", actual.Text)) time.Sleep(time.Millisecond * 10) } if t.Failed() { fmt.Println(out.String()) } } func TestIndeterminateIndicatorMovingBackAndForth(t *testing.T) { maxLen := 10 indicator := "<=>" expectedPositions := []int{ 0, 1, 2, 3, 4, 5, 6, 7, 6, 5, 4, 3, 2, 1, 0, 1, 2, 3, 4, 5, 6, 7, 6, 5, 4, 3, 2, 1, } f := IndeterminateIndicatorMovingBackAndForth(indicator, time.Millisecond*10) for idx, expectedPosition := range expectedPositions { actual := f(maxLen) assert.Equal(t, expectedPosition, actual.Position, fmt.Sprintf("expectedPositions[%d]", idx)) time.Sleep(time.Millisecond * 10) } } func Test_indeterminateIndicatorMovingBackAndForth1(t *testing.T) { maxLen := 10 indicator := "?" expectedPositions := []int{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1, } f := indeterminateIndicatorMovingBackAndForth(indicator) for idx, expectedPosition := range expectedPositions { actual := f(maxLen) assert.Equal(t, expectedPosition, actual.Position, fmt.Sprintf("expectedPositions[%d]", idx)) } } func Test_indeterminateIndicatorMovingBackAndForth2(t *testing.T) { maxLen := 10 indicator := "<>" expectedPositions := []int{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 7, 6, 5, 4, 3, 2, 1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 7, 6, 5, 4, 3, 2, 1, } f := indeterminateIndicatorMovingBackAndForth(indicator) for idx, expectedPosition := range expectedPositions { actual := f(maxLen) assert.Equal(t, expectedPosition, actual.Position, fmt.Sprintf("expectedPositions[%d]", idx)) } } func Test_indeterminateIndicatorMovingBackAndForth3(t *testing.T) { maxLen := 10 indicator := "<=>" expectedPositions := []int{ 0, 1, 2, 3, 4, 5, 6, 7, 6, 5, 4, 3, 2, 1, 0, 1, 2, 3, 4, 5, 6, 7, 6, 5, 4, 3, 2, 1, } f := indeterminateIndicatorMovingBackAndForth(indicator) for idx, expectedPosition := range expectedPositions { actual := f(maxLen) assert.Equal(t, expectedPosition, actual.Position, fmt.Sprintf("expectedPositions[%d]", idx)) } } func TestIndeterminateIndicatorMovingLeftToRight(t *testing.T) { maxLen := 10 indicator := "?" expectedPositions := []int{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, } f := IndeterminateIndicatorMovingLeftToRight(indicator, time.Millisecond*10) for idx, expectedPosition := range expectedPositions { actual := f(maxLen) assert.Equal(t, expectedPosition, actual.Position, fmt.Sprintf("expectedPositions[%d]", idx)) time.Sleep(time.Millisecond * 10) } } func Test_indeterminateIndicatorMovingLeftToRight1(t *testing.T) { maxLen := 10 indicator := "?" expectedPositions := []int{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, } f := indeterminateIndicatorMovingLeftToRight(indicator) for idx, expectedPosition := range expectedPositions { actual := f(maxLen) assert.Equal(t, expectedPosition, actual.Position, fmt.Sprintf("expectedPositions[%d]", idx)) } } func Test_indeterminateIndicatorMovingLeftToRight2(t *testing.T) { maxLen := 10 indicator := "<>" expectedPositions := []int{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 0, 1, 2, 3, 4, 5, 6, 7, 8, } f := indeterminateIndicatorMovingLeftToRight(indicator) for idx, expectedPosition := range expectedPositions { actual := f(maxLen) assert.Equal(t, expectedPosition, actual.Position, fmt.Sprintf("expectedPositions[%d]", idx)) } } func Test_indeterminateIndicatorMovingLeftToRight3(t *testing.T) { maxLen := 10 indicator := "<=>" expectedPositions := []int{ 0, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7, } f := indeterminateIndicatorMovingLeftToRight(indicator) for idx, expectedPosition := range expectedPositions { actual := f(maxLen) assert.Equal(t, expectedPosition, actual.Position, fmt.Sprintf("expectedPositions[%d]", idx)) } } func TestIndeterminateIndicatorMovingRightToLeft(t *testing.T) { maxLen := 10 indicator := "?" expectedPositions := []int{ 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, } f := IndeterminateIndicatorMovingRightToLeft(indicator, time.Millisecond*10) for idx, expectedPosition := range expectedPositions { actual := f(maxLen) assert.Equal(t, expectedPosition, actual.Position, fmt.Sprintf("expectedPositions[%d]", idx)) time.Sleep(time.Millisecond * 10) } } func Test_indeterminateIndicatorMovingRightToLeft1(t *testing.T) { maxLen := 10 indicator := "?" expectedPositions := []int{ 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, } f := indeterminateIndicatorMovingRightToLeft(indicator) for idx, expectedPosition := range expectedPositions { actual := f(maxLen) assert.Equal(t, expectedPosition, actual.Position, fmt.Sprintf("expectedPositions[%d]", idx)) } } func Test_indeterminateIndicatorMovingRightToLeft2(t *testing.T) { maxLen := 10 indicator := "<>" expectedPositions := []int{ 8, 7, 6, 5, 4, 3, 2, 1, 0, 8, 7, 6, 5, 4, 3, 2, 1, 0, } f := indeterminateIndicatorMovingRightToLeft(indicator) for idx, expectedPosition := range expectedPositions { actual := f(maxLen) assert.Equal(t, expectedPosition, actual.Position, fmt.Sprintf("expectedPositions[%d]", idx)) } } func Test_indeterminateIndicatorMovingRightToLeft3(t *testing.T) { maxLen := 10 indicator := "<=>" expectedPositions := []int{ 7, 6, 5, 4, 3, 2, 1, 0, 7, 6, 5, 4, 3, 2, 1, 0, } f := indeterminateIndicatorMovingRightToLeft(indicator) for idx, expectedPosition := range expectedPositions { actual := f(maxLen) assert.Equal(t, expectedPosition, actual.Position, fmt.Sprintf("expectedPositions[%d]", idx)) } } func TestIndeterminateIndicatorPacMan(t *testing.T) { maxLen := 10 expectedTexts := []string{ "ᗧ ", " ᗧ ", " ᗧ ", " ᗧ ", " ᗧ ", " ᗧ ", " ᗧ ", " ᗧ ", " ᗧ ", " ᗧ", " ᗤ ", " ᗤ ", " ᗤ ", " ᗤ ", " ᗤ ", " ᗤ ", " ᗤ ", " ᗤ ", "ᗤ ", " ᗧ ", " ᗧ ", " ᗧ ", " ᗧ ", " ᗧ ", " ᗧ ", " ᗧ ", " ᗧ ", " ᗧ", } out := strings.Builder{} f := IndeterminateIndicatorPacMan(time.Millisecond * 10) for idx, expectedText := range expectedTexts { actual := f(maxLen) assert.Equal(t, expectedText, actual.Text, fmt.Sprintf("expectedTexts[%d]", idx)) out.WriteString(fmt.Sprintf("%#v,\n", actual.Text)) time.Sleep(time.Millisecond * 10) } if t.Failed() { fmt.Println(out.String()) } } go-pretty-6.2.4/progress/progress.go000066400000000000000000000200611407250454200175160ustar00rootroot00000000000000package progress import ( "io" "os" "sync" "time" "unicode/utf8" ) var ( // DefaultLengthTracker defines a sane value for a Tracker's length. DefaultLengthTracker = 20 // DefaultUpdateFrequency defines a sane value for the frequency with which // all the Tracker's get updated on the screen. DefaultUpdateFrequency = time.Millisecond * 250 ) // Progress helps track progress for one or more tasks. type Progress struct { autoStop bool done chan bool lengthTracker int lengthProgress int outputWriter io.Writer hideTime bool hideTracker bool hideValue bool hidePercentage bool messageWidth int numTrackersExpected int64 overallTracker *Tracker overallTrackerMutex sync.RWMutex renderInProgress bool renderInProgressMutex sync.RWMutex showETA bool showOverallTracker bool sortBy SortBy style *Style trackerPosition Position trackersActive []*Tracker trackersActiveMutex sync.RWMutex trackersDone []*Tracker trackersDoneMutex sync.RWMutex trackersInQueue []*Tracker trackersInQueueMutex sync.RWMutex updateFrequency time.Duration } // Position defines the position of the Tracker with respect to the Tracker's // Message. type Position int const ( // PositionLeft will make the Tracker be displayed first before the Message. PositionLeft Position = iota // PositionRight will make the Tracker be displayed after the Message. PositionRight ) // AppendTracker appends a single Tracker for tracking. The Tracker gets added // to a queue, which gets picked up by the Render logic in the next rendering // cycle. func (p *Progress) AppendTracker(t *Tracker) { t.start() p.overallTrackerMutex.Lock() if p.overallTracker == nil { p.overallTracker = &Tracker{Total: 1} if p.numTrackersExpected > 0 { p.overallTracker.Total = p.numTrackersExpected * 100 } p.overallTracker.start() } p.trackersInQueueMutex.Lock() p.trackersInQueue = append(p.trackersInQueue, t) p.trackersInQueueMutex.Unlock() p.overallTracker.mutex.Lock() if p.overallTracker.Total < int64(p.Length())*100 { p.overallTracker.Total = int64(p.Length()) * 100 } p.overallTracker.mutex.Unlock() p.overallTrackerMutex.Unlock() } // AppendTrackers appends one or more Trackers for tracking. func (p *Progress) AppendTrackers(trackers []*Tracker) { for _, tracker := range trackers { p.AppendTracker(tracker) } } // IsRenderInProgress returns true if a call to Render() was made, and is still // in progress and has not ended yet. func (p *Progress) IsRenderInProgress() bool { p.renderInProgressMutex.RLock() defer p.renderInProgressMutex.RUnlock() return p.renderInProgress } // Length returns the number of Trackers tracked overall. func (p *Progress) Length() int { p.trackersActiveMutex.RLock() p.trackersDoneMutex.RLock() p.trackersInQueueMutex.RLock() out := len(p.trackersInQueue) + len(p.trackersActive) + len(p.trackersDone) p.trackersInQueueMutex.RUnlock() p.trackersDoneMutex.RUnlock() p.trackersActiveMutex.RUnlock() return out } // LengthActive returns the number of Trackers actively tracked (not done yet). func (p *Progress) LengthActive() int { p.trackersActiveMutex.RLock() p.trackersInQueueMutex.RLock() out := len(p.trackersInQueue) + len(p.trackersActive) p.trackersInQueueMutex.RUnlock() p.trackersActiveMutex.RUnlock() return out } // LengthDone returns the number of Trackers that are done tracking. func (p *Progress) LengthDone() int { p.trackersDoneMutex.RLock() out := len(p.trackersDone) p.trackersDoneMutex.RUnlock() return out } // LengthInQueue returns the number of Trackers in queue to be actively tracked // (not tracking yet). func (p *Progress) LengthInQueue() int { p.trackersInQueueMutex.RLock() out := len(p.trackersInQueue) p.trackersInQueueMutex.RUnlock() return out } // SetAutoStop toggles the auto-stop functionality. Auto-stop set to true would // mean that the Render() function will automatically stop once all currently // active Trackers reach their final states. When set to false, the client code // will have to call Progress.Stop() to stop the Render() logic. Default: false. func (p *Progress) SetAutoStop(autoStop bool) { p.autoStop = autoStop } // SetMessageWidth sets the (printed) length of the tracker message. Any message // longer the specified width will be snipped abruptly. Any message shorter than // the specified width will be padded with spaces. func (p *Progress) SetMessageWidth(width int) { p.messageWidth = width } // SetNumTrackersExpected sets the expected number of trackers to be tracked. // This helps calculate the overall progress with better accuracy. func (p *Progress) SetNumTrackersExpected(numTrackers int) { p.numTrackersExpected = int64(numTrackers) } // SetOutputWriter redirects the output of Render to an io.writer object like // os.Stdout or os.Stderr or a file. Warning: redirecting the output to a file // may not work well as the Render() logic moves the cursor around a lot. func (p *Progress) SetOutputWriter(writer io.Writer) { p.outputWriter = writer } // SetSortBy defines the sorting mechanism to use to sort the Active Trackers // before rendering the. Default: no-sorting == sort-by-insertion-order. func (p *Progress) SetSortBy(sortBy SortBy) { p.sortBy = sortBy } // SetStyle sets the Style to use for rendering. func (p *Progress) SetStyle(style Style) { p.style = &style } // SetTrackerLength sets the text-length of all the Trackers. func (p *Progress) SetTrackerLength(length int) { p.lengthTracker = length } // SetTrackerPosition sets the position of the tracker with respect to the // Tracker message text. func (p *Progress) SetTrackerPosition(position Position) { p.trackerPosition = position } // SetUpdateFrequency sets the update frequency while rendering the trackers. // the lower the value, the more number of times the Trackers get refreshed. A // sane value would be 250ms. func (p *Progress) SetUpdateFrequency(frequency time.Duration) { p.updateFrequency = frequency } // ShowETA toggles showing the ETA for all individual trackers. func (p *Progress) ShowETA(show bool) { p.showETA = show } // ShowPercentage toggles showing the Percent complete for each Tracker. func (p *Progress) ShowPercentage(show bool) { p.hidePercentage = !show } // ShowOverallTracker toggles showing the Overall progress tracker with an ETA. func (p *Progress) ShowOverallTracker(show bool) { p.showOverallTracker = show } // ShowTime toggles showing the Time taken by each Tracker. func (p *Progress) ShowTime(show bool) { p.hideTime = !show } // ShowTracker toggles showing the Tracker (the progress bar). func (p *Progress) ShowTracker(show bool) { p.hideTracker = !show } // ShowValue toggles showing the actual Value of the Tracker. func (p *Progress) ShowValue(show bool) { p.hideValue = !show } // Stop stops the Render() logic that is in progress. func (p *Progress) Stop() { if p.IsRenderInProgress() { p.done <- true } } // Style returns the current Style. func (p *Progress) Style() *Style { if p.style == nil { tempStyle := StyleDefault p.style = &tempStyle } return p.style } func (p *Progress) initForRender() { // pick a default style p.Style() // reset the signals p.done = make(chan bool, 1) // pick default lengths if no valid ones set if p.lengthTracker <= 0 { p.lengthTracker = DefaultLengthTracker } // calculate length of the actual progress bar by discount the left/right // border/box chars p.lengthProgress = p.lengthTracker - utf8.RuneCountInString(p.style.Chars.BoxLeft) - utf8.RuneCountInString(p.style.Chars.BoxRight) // if not output write has been set, output to STDOUT if p.outputWriter == nil { p.outputWriter = os.Stdout } // pick a sane update frequency if none set if p.updateFrequency <= 0 { p.updateFrequency = DefaultUpdateFrequency } } // renderHint has hints for the Render*() logic type renderHint struct { hideTime bool // hide the time hideValue bool // hide the value isOverallTracker bool // is the Overall Progress tracker } go-pretty-6.2.4/progress/progress_test.go000066400000000000000000000116271407250454200205650ustar00rootroot00000000000000package progress import ( "math" "os" "testing" "time" "github.com/stretchr/testify/assert" ) func TestProgress_AppendTracker(t *testing.T) { p := Progress{} assert.Equal(t, 0, len(p.trackersInQueue)) tracker := &Tracker{} assert.Equal(t, int64(0), tracker.Total) p.AppendTracker(tracker) assert.Equal(t, 1, len(p.trackersInQueue)) assert.Equal(t, int64(0), tracker.Total) tracker2 := &Tracker{Total: -1} assert.Equal(t, int64(-1), tracker2.Total) p.AppendTracker(tracker2) assert.Equal(t, 2, len(p.trackersInQueue)) assert.Equal(t, int64(math.MaxInt64), tracker2.Total) } func TestProgress_AppendTrackers(t *testing.T) { p := Progress{} assert.Equal(t, 0, len(p.trackersInQueue)) p.AppendTrackers([]*Tracker{{}, {}}) assert.Equal(t, 2, len(p.trackersInQueue)) } func TestProgress_IsRenderInProgress(t *testing.T) { p := Progress{} assert.False(t, p.IsRenderInProgress()) p.renderInProgress = true assert.True(t, p.IsRenderInProgress()) } func TestProgress_Length(t *testing.T) { p := Progress{} assert.Equal(t, 0, p.Length()) p.trackersActive = append(p.trackersActive, &Tracker{}) assert.Equal(t, 1, p.Length()) p.trackersInQueue = append(p.trackersInQueue, &Tracker{}) assert.Equal(t, 2, p.Length()) p.trackersDone = append(p.trackersDone, &Tracker{}) assert.Equal(t, 3, p.Length()) } func TestProgress_LengthActive(t *testing.T) { p := Progress{} assert.Equal(t, 0, p.Length()) assert.Equal(t, 0, p.LengthActive()) p.trackersActive = append(p.trackersActive, &Tracker{}) assert.Equal(t, 1, p.Length()) assert.Equal(t, 1, p.LengthActive()) p.trackersInQueue = append(p.trackersInQueue, &Tracker{}) assert.Equal(t, 2, p.Length()) assert.Equal(t, 2, p.LengthActive()) } func TestProgress_LengthDone(t *testing.T) { p := Progress{} assert.Equal(t, 0, p.Length()) assert.Equal(t, 0, p.LengthDone()) p.trackersDone = append(p.trackersDone, &Tracker{}) assert.Equal(t, 1, p.Length()) assert.Equal(t, 1, p.LengthDone()) } func TestProgress_LengthInQueue(t *testing.T) { p := Progress{} assert.Equal(t, 0, p.Length()) assert.Equal(t, 0, p.LengthInQueue()) p.trackersInQueue = append(p.trackersInQueue, &Tracker{}) assert.Equal(t, 1, p.Length()) assert.Equal(t, 1, p.LengthInQueue()) } func TestProgress_SetAutoStop(t *testing.T) { p := Progress{} assert.False(t, p.autoStop) p.SetAutoStop(true) assert.True(t, p.autoStop) } func TestProgress_SetNumTrackersExpected(t *testing.T) { p := Progress{} assert.Equal(t, int64(0), p.numTrackersExpected) p.SetNumTrackersExpected(5) assert.Equal(t, int64(5), p.numTrackersExpected) } func TestProgress_SetOutputWriter(t *testing.T) { p := Progress{} assert.Nil(t, p.outputWriter) p.SetOutputWriter(os.Stdout) assert.Equal(t, os.Stdout, p.outputWriter) } func TestProgress_SetSortBy(t *testing.T) { p := Progress{} assert.Zero(t, p.sortBy) p.SetSortBy(SortByMessage) assert.Equal(t, SortByMessage, p.sortBy) } func TestProgress_SetStyle(t *testing.T) { p := Progress{} assert.Nil(t, p.style) p.SetStyle(StyleCircle) assert.Equal(t, StyleCircle.Name, p.Style().Name) } func TestProgress_SetTrackerLength(t *testing.T) { p := Progress{} assert.Equal(t, 0, p.lengthTracker) p.initForRender() assert.Equal(t, DefaultLengthTracker, p.lengthTracker) p.SetTrackerLength(80) assert.Equal(t, 80, p.lengthTracker) } func TestProgress_SetTrackerPosition(t *testing.T) { p := Progress{} assert.Equal(t, PositionLeft, p.trackerPosition) p.SetTrackerPosition(PositionRight) assert.Equal(t, PositionRight, p.trackerPosition) } func TestProgress_SetUpdateFrequency(t *testing.T) { p := Progress{} assert.Equal(t, time.Duration(0), p.updateFrequency) p.initForRender() assert.Equal(t, DefaultUpdateFrequency, p.updateFrequency) p.SetUpdateFrequency(time.Duration(time.Second)) assert.Equal(t, time.Duration(time.Second), p.updateFrequency) } func TestProgress_ShowOverallTracker(t *testing.T) { p := Progress{} assert.False(t, p.showOverallTracker) p.ShowOverallTracker(true) assert.True(t, p.showOverallTracker) } func TestProgress_ShowPercentage(t *testing.T) { p := Progress{} assert.False(t, p.hidePercentage) p.ShowPercentage(false) assert.True(t, p.hidePercentage) } func TestProgress_ShowTime(t *testing.T) { p := Progress{} assert.False(t, p.hideTime) p.ShowTime(false) assert.True(t, p.hideTime) } func TestProgress_ShowTracker(t *testing.T) { p := Progress{} assert.False(t, p.hideTracker) p.ShowTracker(false) assert.True(t, p.hideTracker) } func TestProgress_ShowValue(t *testing.T) { p := Progress{} assert.False(t, p.hideValue) p.ShowValue(false) assert.True(t, p.hideValue) } func TestProgress_Stop(t *testing.T) { doneChannel := make(chan bool, 1) p := Progress{} p.done = doneChannel p.renderInProgress = true p.Stop() assert.True(t, <-doneChannel) } func TestProgress_Style(t *testing.T) { p := Progress{} assert.Nil(t, p.style) assert.NotNil(t, p.Style()) assert.Equal(t, StyleDefault.Name, p.Style().Name) } go-pretty-6.2.4/progress/render.go000066400000000000000000000253611407250454200171410ustar00rootroot00000000000000package progress import ( "fmt" "math" "strings" "time" "github.com/jedib0t/go-pretty/v6/text" ) // Render renders the Progress tracker and handles all existing trackers and // those that are added dynamically while render is in progress. func (p *Progress) Render() { if p.beginRender() { p.initForRender() lastRenderLength := 0 ticker := time.NewTicker(p.updateFrequency) defer ticker.Stop() for { select { case <-ticker.C: lastRenderLength = p.renderTrackers(lastRenderLength) case <-p.done: // always render the current state before finishing render in // case it hasn't been shown yet lastRenderLength = p.renderTrackers(lastRenderLength) p.endRender() return } } } } func (p *Progress) beginRender() bool { p.renderInProgressMutex.Lock() defer p.renderInProgressMutex.Unlock() if p.renderInProgress { return false } p.renderInProgress = true return true } func (p *Progress) consumeQueuedTrackers() { if p.LengthInQueue() > 0 { p.trackersActiveMutex.Lock() p.trackersInQueueMutex.Lock() p.trackersActive = append(p.trackersActive, p.trackersInQueue...) p.trackersInQueue = make([]*Tracker, 0) p.trackersInQueueMutex.Unlock() p.trackersActiveMutex.Unlock() } } func (p *Progress) endRender() { p.renderInProgressMutex.Lock() defer p.renderInProgressMutex.Unlock() p.renderInProgress = false } func (p *Progress) extractDoneAndActiveTrackers() ([]*Tracker, []*Tracker) { // move trackers waiting in queue to the active list p.consumeQueuedTrackers() // separate the active and done trackers var trackersActive, trackersDone []*Tracker var activeTrackersProgress int64 p.trackersActiveMutex.RLock() for _, tracker := range p.trackersActive { if !tracker.IsDone() { trackersActive = append(trackersActive, tracker) activeTrackersProgress += int64(tracker.PercentDone()) } else { trackersDone = append(trackersDone, tracker) } } p.trackersActiveMutex.RUnlock() p.sortBy.Sort(trackersDone) p.sortBy.Sort(trackersActive) // calculate the overall tracker's progress value p.overallTracker.value = int64(p.LengthDone()+len(trackersDone)) * 100 p.overallTracker.value += activeTrackersProgress if len(trackersActive) == 0 { p.overallTracker.MarkAsDone() } return trackersActive, trackersDone } func (p *Progress) generateTrackerStr(t *Tracker, maxLen int, hint renderHint) string { value, total := t.valueAndTotal() if !hint.isOverallTracker && (total == 0 || value > total) { return p.generateTrackerStrIndeterminate(maxLen) } return p.generateTrackerStrDeterminate(value, total, maxLen) } // generateTrackerStrDeterminate generates the tracker string for the case where // the Total value is known, and the progress percentage can be calculated. func (p *Progress) generateTrackerStrDeterminate(value int64, total int64, maxLen int) string { pFinishedDots, pFinishedDotsFraction := 0.0, 0.0 pDotValue := float64(total) / float64(maxLen) if pDotValue > 0 { pFinishedDots = float64(value) / pDotValue pFinishedDotsFraction = pFinishedDots - float64(int(pFinishedDots)) } pFinishedLen := int(math.Floor(pFinishedDots)) var pFinished, pInProgress, pUnfinished string if pFinishedLen > 0 { pFinished = strings.Repeat(p.style.Chars.Finished, pFinishedLen) } pInProgress = p.style.Chars.Unfinished if pFinishedDotsFraction >= 0.75 { pInProgress = p.style.Chars.Finished75 } else if pFinishedDotsFraction >= 0.50 { pInProgress = p.style.Chars.Finished50 } else if pFinishedDotsFraction >= 0.25 { pInProgress = p.style.Chars.Finished25 } else if pFinishedDotsFraction == 0 { pInProgress = "" } pFinishedStrLen := text.RuneCount(pFinished + pInProgress) if pFinishedStrLen < maxLen { pUnfinished = strings.Repeat(p.style.Chars.Unfinished, maxLen-pFinishedStrLen) } return p.style.Colors.Tracker.Sprintf("%s%s%s%s%s", p.style.Chars.BoxLeft, pFinished, pInProgress, pUnfinished, p.style.Chars.BoxRight, ) } // generateTrackerStrDeterminate generates the tracker string for the case where // the Total value is unknown, and the progress percentage cannot be calculated. func (p *Progress) generateTrackerStrIndeterminate(maxLen int) string { indicator := p.style.Chars.Indeterminate(maxLen) pUnfinished := "" if indicator.Position > 0 { pUnfinished += strings.Repeat(p.style.Chars.Unfinished, indicator.Position) } pUnfinished += indicator.Text if text.RuneCount(pUnfinished) < maxLen { pUnfinished += strings.Repeat(p.style.Chars.Unfinished, maxLen-text.RuneCount(pUnfinished)) } return p.style.Colors.Tracker.Sprintf("%s%s%s", p.style.Chars.BoxLeft, string(pUnfinished), p.style.Chars.BoxRight, ) } func (p *Progress) moveCursorToTheTop(out *strings.Builder) { numLinesToMoveUp := len(p.trackersActive) if p.showOverallTracker && p.overallTracker != nil && !p.overallTracker.IsDone() { numLinesToMoveUp++ } if numLinesToMoveUp > 0 { out.WriteString(text.CursorUp.Sprintn(numLinesToMoveUp)) } } func (p *Progress) renderTracker(out *strings.Builder, t *Tracker, hint renderHint) { message := t.message() if strings.Contains(message, "\t") { message = strings.Replace(message, "\t", " ", -1) } if strings.Contains(message, "\r") { message = strings.Replace(message, "\r", "", -1) } if p.messageWidth > 0 { messageLen := text.RuneCount(message) if messageLen < p.messageWidth { message = text.Pad(message, p.messageWidth, ' ') } else { message = text.Snip(message, p.messageWidth, p.style.Options.SnipIndicator) } } out.WriteString(text.EraseLine.Sprint()) if hint.isOverallTracker { if !t.IsDone() { trackerLen := p.messageWidth trackerLen += text.RuneCount(p.style.Options.Separator) trackerLen += text.RuneCount(p.style.Options.DoneString) trackerLen += p.lengthProgress + 1 hint := renderHint{hideValue: true, isOverallTracker: true} p.renderTrackerProgress(out, t, message, p.generateTrackerStr(t, trackerLen, hint), hint) } } else { if t.IsDone() { p.renderTrackerDone(out, t, message) } else { hint := renderHint{hideTime: p.hideTime, hideValue: p.hideValue} p.renderTrackerProgress(out, t, message, p.generateTrackerStr(t, p.lengthProgress, hint), hint) } } } func (p *Progress) renderTrackerDone(out *strings.Builder, t *Tracker, message string) { out.WriteString(p.style.Colors.Message.Sprint(message)) out.WriteString(p.style.Colors.Message.Sprint(p.style.Options.Separator)) if !t.IsErrored() { out.WriteString(p.style.Colors.Message.Sprint(p.style.Options.DoneString)) } else { out.WriteString(p.style.Colors.Error.Sprint(p.style.Options.ErrorString)) } p.renderTrackerStats(out, t, renderHint{hideTime: p.hideTime, hideValue: p.hideValue}) out.WriteRune('\n') } func (p *Progress) renderTrackerMessage(out *strings.Builder, t *Tracker, message string) { if !t.IsErrored() { out.WriteString(p.style.Colors.Message.Sprint(message)) } else { out.WriteString(p.style.Colors.Error.Sprint(message)) } } func (p *Progress) renderTrackerPercentage(out *strings.Builder, t *Tracker) { if !p.hidePercentage { var percentageStr string if t.IsIndeterminate() { percentageStr = p.style.Options.PercentIndeterminate } else { percentageStr = fmt.Sprintf(p.style.Options.PercentFormat, t.PercentDone()) } out.WriteString(p.style.Colors.Percent.Sprint(percentageStr)) } } func (p *Progress) renderTrackerProgress(out *strings.Builder, t *Tracker, message string, trackerStr string, hint renderHint) { if hint.isOverallTracker { out.WriteString(p.style.Colors.Tracker.Sprint(trackerStr)) p.renderTrackerStats(out, t, hint) out.WriteRune('\n') } else if p.trackerPosition == PositionRight { p.renderTrackerMessage(out, t, message) out.WriteString(p.style.Colors.Message.Sprint(p.style.Options.Separator)) p.renderTrackerPercentage(out, t) if !p.hideTracker { out.WriteString(p.style.Colors.Tracker.Sprint(" " + trackerStr)) } p.renderTrackerStats(out, t, hint) out.WriteRune('\n') } else { p.renderTrackerPercentage(out, t) if !p.hideTracker { out.WriteString(p.style.Colors.Tracker.Sprint(" " + trackerStr)) } p.renderTrackerStats(out, t, hint) out.WriteString(p.style.Colors.Message.Sprint(p.style.Options.Separator)) p.renderTrackerMessage(out, t, message) out.WriteRune('\n') } } func (p *Progress) renderTrackers(lastRenderLength int) int { if p.LengthActive() == 0 { return 0 } // buffer all output into a strings.Builder object var out strings.Builder out.Grow(lastRenderLength) // move up N times based on the number of active trackers if lastRenderLength > 0 { p.moveCursorToTheTop(&out) } // find the currently "active" and "done" trackers trackersActive, trackersDone := p.extractDoneAndActiveTrackers() // sort and render the done trackers for _, tracker := range trackersDone { p.renderTracker(&out, tracker, renderHint{}) } p.trackersDoneMutex.Lock() p.trackersDone = append(p.trackersDone, trackersDone...) p.trackersDoneMutex.Unlock() // sort and render the active trackers for _, tracker := range trackersActive { p.renderTracker(&out, tracker, renderHint{}) } p.trackersActiveMutex.Lock() p.trackersActive = trackersActive p.trackersActiveMutex.Unlock() // render the overall tracker if p.showOverallTracker { p.renderTracker(&out, p.overallTracker, renderHint{isOverallTracker: true}) } // write the text to the output writer _, _ = p.outputWriter.Write([]byte(out.String())) // stop if auto stop is enabled and there are no more active trackers if p.autoStop && p.LengthActive() == 0 { p.done <- true } return out.Len() } func (p *Progress) renderTrackerStats(out *strings.Builder, t *Tracker, hint renderHint) { if !hint.hideValue || !hint.hideTime { var outStats strings.Builder outStats.WriteString(" [") if !hint.hideValue { outStats.WriteString(p.style.Colors.Value.Sprint(t.Units.Sprint(t.Value()))) } if !hint.hideValue && !hint.hideTime { outStats.WriteString(" in ") } if !hint.hideTime { var td, tp time.Duration if t.IsDone() { td = t.timeStop.Sub(t.timeStart) } else { td = time.Since(t.timeStart) } if hint.isOverallTracker { tp = p.style.Options.TimeOverallPrecision } else if t.IsDone() { tp = p.style.Options.TimeDonePrecision } else { tp = p.style.Options.TimeInProgressPrecision } outStats.WriteString(p.style.Colors.Time.Sprint(td.Round(tp))) if p.showETA || hint.isOverallTracker { p.renderTrackerStatsETA(&outStats, t, hint) } } outStats.WriteRune(']') out.WriteString(p.style.Colors.Stats.Sprint(outStats.String())) } } func (p *Progress) renderTrackerStatsETA(out *strings.Builder, t *Tracker, hint renderHint) { tpETA := p.style.Options.ETAPrecision if eta := t.ETA().Round(tpETA); hint.isOverallTracker || eta > tpETA { out.WriteString("; ") out.WriteString(p.style.Options.ETAString) out.WriteString(": ") out.WriteString(p.style.Colors.Time.Sprint(eta)) } } go-pretty-6.2.4/progress/render_test.go000066400000000000000000000526211407250454200201770ustar00rootroot00000000000000package progress import ( "fmt" "regexp" "sort" "strings" "testing" "time" "github.com/stretchr/testify/assert" ) type outputWriter struct { Text strings.Builder } func (w *outputWriter) Write(p []byte) (n int, err error) { return w.Text.Write(p) } func (w *outputWriter) String() string { return w.Text.String() } func generateWriter() Writer { pw := NewWriter() pw.SetAutoStop(false) pw.SetNumTrackersExpected(1) pw.SetSortBy(SortByNone) pw.SetStyle(StyleDefault) pw.SetTrackerLength(25) pw.SetTrackerPosition(PositionRight) pw.SetUpdateFrequency(time.Millisecond * 50) pw.ShowOverallTracker(false) pw.ShowPercentage(true) pw.ShowTime(true) pw.ShowTracker(true) pw.ShowValue(true) pw.Style().Colors = StyleColors{} pw.Style().Options = StyleOptionsDefault return pw } func trackSomething(pw Writer, tracker *Tracker) { incrementPerCycle := tracker.Total / 3 pw.AppendTracker(tracker) c := time.Tick(time.Millisecond * 100) for !tracker.IsDone() { select { case <-c: if tracker.value+incrementPerCycle > tracker.Total { tracker.Increment(tracker.Total - tracker.value) } else { tracker.Increment(incrementPerCycle) } } } } func trackSomethingErrored(pw Writer, tracker *Tracker) { incrementPerCycle := tracker.Total / 3 total := tracker.Total tracker.Total = 0 pw.AppendTracker(tracker) c := time.Tick(time.Millisecond * 100) for !tracker.IsDone() { select { case <-c: if tracker.value+incrementPerCycle > total { tracker.MarkAsErrored() } else { tracker.IncrementWithError(incrementPerCycle) } } } } func trackSomethingIndeterminate(pw Writer, tracker *Tracker) { incrementPerCycle := tracker.Total / 3 total := tracker.Total tracker.Total = 0 pw.AppendTracker(tracker) c := time.Tick(time.Millisecond * 100) for !tracker.IsDone() { select { case <-c: if tracker.value+incrementPerCycle > total { tracker.Increment(total - tracker.value) } else { tracker.Increment(incrementPerCycle) } if tracker.Value() >= total { tracker.MarkAsDone() } } } } func renderAndWait(pw Writer, autoStop bool) { go pw.Render() go pw.Render() // this call should be a no-op time.Sleep(time.Millisecond * 100) for pw.IsRenderInProgress() { if pw.LengthActive() == 0 { break } time.Sleep(time.Millisecond * 100) } if !autoStop { pw.Stop() } } func showOutputOnFailure(t *testing.T, out string) { if t.Failed() { lines := strings.Split(out, "\n") sort.Strings(lines) for _, line := range lines { fmt.Printf("%#v,\n", line) } } } func TestProgress_generateTrackerStr(t *testing.T) { pw := Progress{} pw.Style().Chars = StyleChars{ BoxLeft: "", BoxRight: "", Finished: "#", Finished25: "1", Finished50: "2", Finished75: "3", Unfinished: ".", } expectedTrackerStrMap := map[int64]string{ 0: "..........", 1: "..........", 2: "..........", 3: "1.........", 4: "1.........", 5: "2.........", 6: "2.........", 7: "2.........", 8: "3.........", 9: "3.........", 10: "#.........", 11: "#.........", 12: "#.........", 13: "#1........", 14: "#1........", 15: "#2........", 16: "#2........", 17: "#2........", 18: "#3........", 19: "#3........", 20: "##........", 21: "##........", 22: "##........", 23: "##1.......", 24: "##1.......", 25: "##2.......", 26: "##2.......", 27: "##2.......", 28: "##3.......", 29: "##3.......", 30: "###.......", 31: "###.......", 32: "###.......", 33: "###1......", 34: "###1......", 35: "###2......", 36: "###2......", 37: "###2......", 38: "###3......", 39: "###3......", 40: "####......", 41: "####......", 42: "####......", 43: "####1.....", 44: "####1.....", 45: "####2.....", 46: "####2.....", 47: "####2.....", 48: "####3.....", 49: "####3.....", 50: "#####.....", 51: "#####.....", 52: "#####.....", 53: "#####1....", 54: "#####1....", 55: "#####2....", 56: "#####2....", 57: "#####2....", 58: "#####3....", 59: "#####3....", 60: "######....", 61: "######....", 62: "######....", 63: "######1...", 64: "######1...", 65: "######2...", 66: "######2...", 67: "######2...", 68: "######3...", 69: "######3...", 70: "#######...", 71: "#######...", 72: "#######...", 73: "#######1..", 74: "#######1..", 75: "#######2..", 76: "#######2..", 77: "#######2..", 78: "#######3..", 79: "#######3..", 80: "########..", 81: "########..", 82: "########..", 83: "########1.", 84: "########1.", 85: "########2.", 86: "########2.", 87: "########2.", 88: "########3.", 89: "########3.", 90: "#########.", 91: "#########.", 92: "#########.", 93: "#########1", 94: "#########1", 95: "#########2", 96: "#########2", 97: "#########2", 98: "#########3", 99: "#########3", 100: "##########", } finalOutput := strings.Builder{} tr := Tracker{Total: 100} for value := int64(0); value <= 100; value++ { tr.value = value actualStr := pw.generateTrackerStr(&tr, 10, renderHint{}) if expectedStr, ok := expectedTrackerStrMap[value]; ok { assert.Equal(t, expectedStr, actualStr, "value=%d", value) } finalOutput.WriteString(fmt.Sprintf(" %d: \"%s\",\n", value, actualStr)) } if t.Failed() { fmt.Println(finalOutput.String()) } } func TestProgress_generateTrackerStr_Indeterminate(t *testing.T) { pw := Progress{} pw.Style().Chars = StyleChars{ BoxLeft: "", BoxRight: "", Finished: "#", Finished25: "1", Finished50: "2", Finished75: "3", Indeterminate: indeterminateIndicatorMovingBackAndForth("<=>"), Unfinished: ".", } expectedTrackerStrMap := map[int64]string{ 0: "<=>.......", 1: ".<=>......", 2: "..<=>.....", 3: "...<=>....", 4: "....<=>...", 5: ".....<=>..", 6: "......<=>.", 7: ".......<=>", 8: "......<=>.", 9: ".....<=>..", 10: "....<=>...", 11: "...<=>....", 12: "..<=>.....", 13: ".<=>......", 14: "<=>.......", 15: ".<=>......", 16: "..<=>.....", 17: "...<=>....", 18: "....<=>...", 19: ".....<=>..", 20: "......<=>.", 21: ".......<=>", 22: "......<=>.", 23: ".....<=>..", 24: "....<=>...", 25: "...<=>....", 26: "..<=>.....", 27: ".<=>......", 28: "<=>.......", 29: ".<=>......", 30: "..<=>.....", 31: "...<=>....", 32: "....<=>...", 33: ".....<=>..", 34: "......<=>.", 35: ".......<=>", 36: "......<=>.", 37: ".....<=>..", 38: "....<=>...", 39: "...<=>....", 40: "..<=>.....", 41: ".<=>......", 42: "<=>.......", 43: ".<=>......", 44: "..<=>.....", 45: "...<=>....", 46: "....<=>...", 47: ".....<=>..", 48: "......<=>.", 49: ".......<=>", 50: "......<=>.", 51: ".....<=>..", 52: "....<=>...", 53: "...<=>....", 54: "..<=>.....", 55: ".<=>......", 56: "<=>.......", 57: ".<=>......", 58: "..<=>.....", 59: "...<=>....", 60: "....<=>...", 61: ".....<=>..", 62: "......<=>.", 63: ".......<=>", 64: "......<=>.", 65: ".....<=>..", 66: "....<=>...", 67: "...<=>....", 68: "..<=>.....", 69: ".<=>......", 70: "<=>.......", 71: ".<=>......", 72: "..<=>.....", 73: "...<=>....", 74: "....<=>...", 75: ".....<=>..", 76: "......<=>.", 77: ".......<=>", 78: "......<=>.", 79: ".....<=>..", 80: "....<=>...", 81: "...<=>....", 82: "..<=>.....", 83: ".<=>......", 84: "<=>.......", 85: ".<=>......", 86: "..<=>.....", 87: "...<=>....", 88: "....<=>...", 89: ".....<=>..", 90: "......<=>.", 91: ".......<=>", 92: "......<=>.", 93: ".....<=>..", 94: "....<=>...", 95: "...<=>....", 96: "..<=>.....", 97: ".<=>......", 98: "<=>.......", 99: ".<=>......", 100: "..<=>.....", } finalOutput := strings.Builder{} tr := Tracker{Total: 0} for value := int64(0); value <= 100; value++ { tr.value = value actualStr := pw.generateTrackerStr(&tr, 10, renderHint{}) if expectedStr, ok := expectedTrackerStrMap[value]; ok { assert.Equal(t, expectedStr, actualStr, "value=%d", value) } finalOutput.WriteString(fmt.Sprintf(" %d: \"%s\",\n", value, actualStr)) } if t.Failed() { fmt.Println(finalOutput.String()) } } func TestProgress_RenderNothing(t *testing.T) { renderOutput := outputWriter{} pw := generateWriter() pw.SetOutputWriter(&renderOutput) go pw.Render() time.Sleep(time.Second) pw.Stop() time.Sleep(time.Second) assert.Empty(t, renderOutput.String()) } func TestProgress_RenderSomeTrackers_OnLeftSide(t *testing.T) { renderOutput := outputWriter{} pw := generateWriter() pw.SetOutputWriter(&renderOutput) pw.SetTrackerPosition(PositionLeft) go trackSomething(pw, &Tracker{Message: "Calculating Total # 1\r", Total: 1000, Units: UnitsDefault}) go trackSomething(pw, &Tracker{Message: "Downloading File\t# 2", Total: 1000, Units: UnitsBytes}) go trackSomething(pw, &Tracker{Message: "Transferring Amount # 3", Total: 1000, Units: UnitsCurrencyDollar}) renderAndWait(pw, false) expectedOutPatterns := []*regexp.Regexp{ regexp.MustCompile(`\x1b\[K\d+\.\d+% \[[#.]{23}] \[\d+ in [\d.]+ms] \.\.\. Calculating Total # 1`), regexp.MustCompile(`\x1b\[K\d+\.\d+% \[[#.]{23}] \[\d+B in [\d.]+ms] \.\.\. Downloading File # 2`), regexp.MustCompile(`\x1b\[K\d+\.\d+% \[[#.]{23}] \[\$\d+ in [\d.]+ms] \.\.\. Transferring Amount # 3`), regexp.MustCompile(`\x1b\[KCalculating Total # 1 \.\.\. done! \[\d+\.\d+K in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KDownloading File # 2 \.\.\. done! \[\d+\.\d+KB in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KTransferring Amount # 3 \.\.\. done! \[\$\d+\.\d+K in [\d.]+ms]`), } out := renderOutput.String() for _, expectedOutPattern := range expectedOutPatterns { if !expectedOutPattern.MatchString(out) { assert.Fail(t, "Failed to find a pattern in the Output.", expectedOutPattern.String()) } } showOutputOnFailure(t, out) } func TestProgress_RenderSomeTrackers_OnRightSide(t *testing.T) { renderOutput := outputWriter{} pw := generateWriter() pw.SetOutputWriter(&renderOutput) pw.SetTrackerPosition(PositionRight) go trackSomething(pw, &Tracker{Message: "Calculating Total # 1\r", Total: 1000, Units: UnitsDefault}) go trackSomething(pw, &Tracker{Message: "Downloading File\t# 2", Total: 1000, Units: UnitsBytes}) go trackSomething(pw, &Tracker{Message: "Transferring Amount # 3", Total: 1000, Units: UnitsCurrencyDollar}) renderAndWait(pw, false) expectedOutPatterns := []*regexp.Regexp{ regexp.MustCompile(`\x1b\[KCalculating Total # 1 \.\.\. \d+\.\d+% \[[#.]{23}] \[\d+ in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KDownloading File # 2 \.\.\. \d+\.\d+% \[[#.]{23}] \[\d+B in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KTransferring Amount # 3 \.\.\. \d+\.\d+% \[[#.]{23}] \[\$\d+ in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KCalculating Total # 1 \.\.\. done! \[\d+\.\d+K in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KDownloading File # 2 \.\.\. done! \[\d+\.\d+KB in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KTransferring Amount # 3 \.\.\. done! \[\$\d+\.\d+K in [\d.]+ms]`), } out := renderOutput.String() for _, expectedOutPattern := range expectedOutPatterns { if !expectedOutPattern.MatchString(out) { assert.Fail(t, "Failed to find a pattern in the Output.", expectedOutPattern.String()) } } showOutputOnFailure(t, out) } func TestProgress_RenderSomeTrackers_WithAutoStop(t *testing.T) { renderOutput := outputWriter{} pw := generateWriter() pw.SetAutoStop(true) pw.SetOutputWriter(&renderOutput) pw.SetTrackerPosition(PositionRight) go trackSomething(pw, &Tracker{Message: "Calculating Total # 1\r", Total: 1000, Units: UnitsDefault}) go trackSomething(pw, &Tracker{Message: "Downloading File\t# 2", Total: 1000, Units: UnitsBytes}) go trackSomething(pw, &Tracker{Message: "Transferring Amount # 3", Total: 1000, Units: UnitsCurrencyDollar}) renderAndWait(pw, true) expectedOutPatterns := []*regexp.Regexp{ regexp.MustCompile(`\x1b\[KCalculating Total # 1 \.\.\. \d+\.\d+% \[[#.]{23}] \[\d+ in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KDownloading File # 2 \.\.\. \d+\.\d+% \[[#.]{23}] \[\d+B in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KTransferring Amount # 3 \.\.\. \d+\.\d+% \[[#.]{23}] \[\$\d+ in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KCalculating Total # 1 \.\.\. done! \[\d+\.\d+K in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KDownloading File # 2 \.\.\. done! \[\d+\.\d+KB in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KTransferring Amount # 3 \.\.\. done! \[\$\d+\.\d+K in [\d.]+ms]`), } out := renderOutput.String() for _, expectedOutPattern := range expectedOutPatterns { if !expectedOutPattern.MatchString(out) { assert.Fail(t, "Failed to find a pattern in the Output.", expectedOutPattern.String()) } } showOutputOnFailure(t, out) } func TestProgress_RenderSomeTrackers_WithError(t *testing.T) { renderOutput := outputWriter{} pw := generateWriter() pw.SetOutputWriter(&renderOutput) go trackSomething(pw, &Tracker{Message: "Calculating Total # 1\r", Total: 1000, Units: UnitsDefault}) go trackSomething(pw, &Tracker{Message: "Downloading File\t# 2", Total: 1000, Units: UnitsBytes}) go trackSomethingErrored(pw, &Tracker{Message: "Transferring Amount # 3", Total: 1000, Units: UnitsCurrencyDollar}) renderAndWait(pw, false) expectedOutPatterns := []*regexp.Regexp{ regexp.MustCompile(`\x1b\[KCalculating Total # 1 \.\.\. \d+\.\d+% \[[#.]{23}] \[\d+ in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KDownloading File # 2 \.\.\. \d+\.\d+% \[[#.]{23}] \[\d+B in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KTransferring Amount # 3 \.\.\. \?\?\? \[[<#>.]{23}] \[\$\d+ in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KCalculating Total # 1 \.\.\. done! \[\d+\.\d+K in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KDownloading File # 2 \.\.\. done! \[\d+\.\d+KB in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KTransferring Amount # 3 \.\.\. fail! \[\$\d+ in [\d.]+ms]`), } out := renderOutput.String() for _, expectedOutPattern := range expectedOutPatterns { if !expectedOutPattern.MatchString(out) { assert.Fail(t, "Failed to find a pattern in the Output.", expectedOutPattern.String()) } } showOutputOnFailure(t, out) } func TestProgress_RenderSomeTrackers_WithIndeterminateTracker(t *testing.T) { renderOutput := outputWriter{} pw := generateWriter() pw.SetOutputWriter(&renderOutput) go trackSomething(pw, &Tracker{Message: "Calculating Total # 1\r", Total: 1000, Units: UnitsDefault}) go trackSomething(pw, &Tracker{Message: "Downloading File\t# 2", Total: 1000, Units: UnitsBytes}) go trackSomethingIndeterminate(pw, &Tracker{Message: "Transferring Amount # 3", Total: 1000, Units: UnitsCurrencyDollar}) renderAndWait(pw, false) expectedOutPatterns := []*regexp.Regexp{ regexp.MustCompile(`\x1b\[KCalculating Total # 1 \.\.\. \d+\.\d+% \[[#.]{23}] \[\d+ in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KDownloading File # 2 \.\.\. \d+\.\d+% \[[#.]{23}] \[\d+B in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KTransferring Amount # 3 \.\.\. \?\?\? \[[<#>.]{23}] \[\$\d+ in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KCalculating Total # 1 \.\.\. done! \[\d+\.\d+K in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KDownloading File # 2 \.\.\. done! \[\d+\.\d+KB in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KTransferring Amount # 3 \.\.\. done! \[\$\d+\.\d+K in [\d.]+ms]`), } out := renderOutput.String() for _, expectedOutPattern := range expectedOutPatterns { if !expectedOutPattern.MatchString(out) { assert.Fail(t, "Failed to find a pattern in the Output.", expectedOutPattern.String()) } } showOutputOnFailure(t, out) } func TestProgress_RenderSomeTrackers_WithLineWidth1(t *testing.T) { renderOutput := outputWriter{} pw := generateWriter() pw.SetMessageWidth(5) pw.SetOutputWriter(&renderOutput) pw.SetTrackerPosition(PositionRight) go trackSomething(pw, &Tracker{Message: "Calculating Total # 1\r", Total: 1000, Units: UnitsDefault}) go trackSomething(pw, &Tracker{Message: "Downloading File\t# 2", Total: 1000, Units: UnitsBytes}) go trackSomething(pw, &Tracker{Message: "Transferring Amount # 3", Total: 1000, Units: UnitsCurrencyDollar}) renderAndWait(pw, false) expectedOutPatterns := []*regexp.Regexp{ regexp.MustCompile(`\x1b\[KCalc~ \.\.\. \d+\.\d+% \[[#.]{23}] \[\d+ in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KDown~ \.\.\. \d+\.\d+% \[[#.]{23}] \[\d+B in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KTran~ \.\.\. \d+\.\d+% \[[#.]{23}] \[\$\d+ in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KCalc~ \.\.\. done! \[\d+\.\d+K in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KDown~ \.\.\. done! \[\d+\.\d+KB in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KTran~ \.\.\. done! \[\$\d+\.\d+K in [\d.]+ms]`), } out := renderOutput.String() for _, expectedOutPattern := range expectedOutPatterns { if !expectedOutPattern.MatchString(out) { assert.Fail(t, "Failed to find a pattern in the Output.", expectedOutPattern.String()) } } showOutputOnFailure(t, out) } func TestProgress_RenderSomeTrackers_WithLineWidth2(t *testing.T) { renderOutput := outputWriter{} pw := generateWriter() pw.SetMessageWidth(50) pw.SetOutputWriter(&renderOutput) pw.SetTrackerPosition(PositionRight) go trackSomething(pw, &Tracker{Message: "Calculating Total # 1\r", Total: 1000, Units: UnitsDefault}) go trackSomething(pw, &Tracker{Message: "Downloading File\t# 2", Total: 1000, Units: UnitsBytes}) go trackSomething(pw, &Tracker{Message: "Transferring Amount # 3", Total: 1000, Units: UnitsCurrencyDollar}) renderAndWait(pw, false) expectedOutPatterns := []*regexp.Regexp{ regexp.MustCompile(`\x1b\[KCalculating Total # 1\s{28}\.\.\. \d+\.\d+% \[[#.]{23}] \[\d+ in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KDownloading File # 2\s{28}\.\.\. \d+\.\d+% \[[#.]{23}] \[\d+B in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KTransferring Amount # 3\s{28}\.\.\. \d+\.\d+% \[[#.]{23}] \[\$\d+ in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KCalculating Total # 1\s{28}\.\.\. done! \[\d+\.\d+K in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KDownloading File # 2\s{28}\.\.\. done! \[\d+\.\d+KB in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KTransferring Amount # 3\s{28}\.\.\. done! \[\$\d+\.\d+K in [\d.]+ms]`), } out := renderOutput.String() for _, expectedOutPattern := range expectedOutPatterns { if !expectedOutPattern.MatchString(out) { assert.Fail(t, "Failed to find a pattern in the Output.", expectedOutPattern.String()) } } showOutputOnFailure(t, out) } func TestProgress_RenderSomeTrackers_WithOverallTracker(t *testing.T) { renderOutput := outputWriter{} pw := generateWriter() pw.SetOutputWriter(&renderOutput) pw.SetTrackerPosition(PositionRight) pw.ShowOverallTracker(true) pw.Style().Options.TimeOverallPrecision = time.Millisecond go trackSomething(pw, &Tracker{Message: "Calculating Total # 1\r", Total: 1000, Units: UnitsDefault}) go trackSomething(pw, &Tracker{Message: "Downloading File\t# 2", Total: 1000, Units: UnitsBytes}) go trackSomething(pw, &Tracker{Message: "Transferring Amount # 3", Total: 1000, Units: UnitsCurrencyDollar}) renderAndWait(pw, false) expectedOutPatterns := []*regexp.Regexp{ regexp.MustCompile(`\x1b\[KCalculating Total # 1 \.\.\. \d+\.\d+% \[[#.]{23}] \[\d+ in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KDownloading File # 2 \.\.\. \d+\.\d+% \[[#.]{23}] \[\d+B in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KTransferring Amount # 3 \.\.\. \d+\.\d+% \[[#.]{23}] \[\$\d+ in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KCalculating Total # 1 \.\.\. done! \[\d+\.\d+K in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KDownloading File # 2 \.\.\. done! \[\d+\.\d+KB in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KTransferring Amount # 3 \.\.\. done! \[\$\d+\.\d+K in [\d.]+ms]`), regexp.MustCompile(`\x1b\[K\[[.#]+] \[[\d.ms]+; ~ETA: [\d.ms]+`), } out := renderOutput.String() for _, expectedOutPattern := range expectedOutPatterns { if !expectedOutPattern.MatchString(out) { assert.Fail(t, "Failed to find a pattern in the Output.", expectedOutPattern.String()) } } showOutputOnFailure(t, out) } func TestProgress_RenderSomeTrackers_WithoutOverallTracker_WithETA(t *testing.T) { renderOutput := outputWriter{} pw := generateWriter() pw.SetOutputWriter(&renderOutput) pw.SetTrackerPosition(PositionRight) pw.ShowETA(true) pw.ShowOverallTracker(false) pw.Style().Options.ETAPrecision = time.Millisecond go trackSomething(pw, &Tracker{Message: "Calculating Total # 1\r", Total: 1000, Units: UnitsDefault}) go trackSomething(pw, &Tracker{Message: "Downloading File\t# 2", Total: 1000, Units: UnitsBytes}) go trackSomething(pw, &Tracker{Message: "Transferring Amount # 3", Total: 1000, Units: UnitsCurrencyDollar}) renderAndWait(pw, false) expectedOutPatterns := []*regexp.Regexp{ regexp.MustCompile(`\x1b\[KCalculating Total # 1 \.\.\. \d+\.\d+% \[[#.]{23}] \[\d+ in [\d.]+ms; ~ETA: [\d]+ms]`), regexp.MustCompile(`\x1b\[KDownloading File # 2 \.\.\. \d+\.\d+% \[[#.]{23}] \[\d+B in [\d.]+ms; ~ETA: [\d]+ms]`), regexp.MustCompile(`\x1b\[KTransferring Amount # 3 \.\.\. \d+\.\d+% \[[#.]{23}] \[\$\d+ in [\d.]+ms; ~ETA: [\d]+ms]`), regexp.MustCompile(`\x1b\[KCalculating Total # 1 \.\.\. done! \[\d+\.\d+K in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KDownloading File # 2 \.\.\. done! \[\d+\.\d+KB in [\d.]+ms]`), regexp.MustCompile(`\x1b\[KTransferring Amount # 3 \.\.\. done! \[\$\d+\.\d+K in [\d.]+ms]`), } out := renderOutput.String() for _, expectedOutPattern := range expectedOutPatterns { if !expectedOutPattern.MatchString(out) { assert.Fail(t, "Failed to find a pattern in the Output.", expectedOutPattern.String()) } } showOutputOnFailure(t, out) } go-pretty-6.2.4/progress/style.go000066400000000000000000000127171407250454200170230ustar00rootroot00000000000000package progress import ( "time" "github.com/jedib0t/go-pretty/v6/text" ) // Style declares how to render the Progress/Trackers. type Style struct { Name string // name of the Style Chars StyleChars // characters to use on the progress bar Colors StyleColors // colors to use on the progress bar Options StyleOptions // misc. options for the progress bar } var ( // StyleDefault uses ASCII text to render the Trackers. StyleDefault = Style{ Name: "StyleDefault", Chars: StyleCharsDefault, Colors: StyleColorsDefault, Options: StyleOptionsDefault, } // StyleBlocks uses UNICODE Block Drawing characters to render the Trackers. StyleBlocks = Style{ Name: "StyleBlocks", Chars: StyleCharsBlocks, Colors: StyleColorsDefault, Options: StyleOptionsDefault, } // StyleCircle uses UNICODE Circle runes to render the Trackers. StyleCircle = Style{ Name: "StyleCircle", Chars: StyleCharsCircle, Colors: StyleColorsDefault, Options: StyleOptionsDefault, } // StyleRhombus uses UNICODE Rhombus runes to render the Trackers. StyleRhombus = Style{ Name: "StyleRhombus", Chars: StyleCharsRhombus, Colors: StyleColorsDefault, Options: StyleOptionsDefault, } ) // StyleChars defines the characters/strings to use for rendering the Tracker. type StyleChars struct { BoxLeft string // left-border BoxRight string // right-border Finished string // finished block Finished25 string // 25% finished block Finished50 string // 50% finished block Finished75 string // 75% finished block Indeterminate IndeterminateIndicatorGenerator Unfinished string // 0% finished block } var ( // StyleCharsDefault uses simple ASCII characters. StyleCharsDefault = StyleChars{ BoxLeft: "[", BoxRight: "]", Finished: "#", Finished25: ".", Finished50: ".", Finished75: ".", Indeterminate: IndeterminateIndicatorMovingBackAndForth("<#>", DefaultUpdateFrequency/2), Unfinished: ".", } // StyleCharsBlocks uses UNICODE Block Drawing characters. StyleCharsBlocks = StyleChars{ BoxLeft: "║", BoxRight: "║", Finished: "█", Finished25: "░", Finished50: "▒", Finished75: "▓", Indeterminate: IndeterminateIndicatorMovingBackAndForth("▒█▒", DefaultUpdateFrequency/2), Unfinished: "░", } // StyleCharsCircle uses UNICODE Circle characters. StyleCharsCircle = StyleChars{ BoxLeft: "(", BoxRight: ")", Finished: "●", Finished25: "○", Finished50: "○", Finished75: "○", Indeterminate: IndeterminateIndicatorMovingBackAndForth("○●○", DefaultUpdateFrequency/2), Unfinished: "◌", } // StyleCharsRhombus uses UNICODE Rhombus characters. StyleCharsRhombus = StyleChars{ BoxLeft: "<", BoxRight: ">", Finished: "◆", Finished25: "◈", Finished50: "◈", Finished75: "◈", Indeterminate: IndeterminateIndicatorMovingBackAndForth("◈◆◈", DefaultUpdateFrequency/2), Unfinished: "◇", } ) // StyleColors defines what colors to use for various parts of the Progress and // Tracker texts. type StyleColors struct { Message text.Colors // message text colors Error text.Colors // error text colors Percent text.Colors // percentage text colors Stats text.Colors // stats text (time, value) colors Time text.Colors // time text colors (overrides Stats) Tracker text.Colors // tracker text colors Value text.Colors // value text colors (overrides Stats) } var ( // StyleColorsDefault defines sane color choices - None. StyleColorsDefault = StyleColors{} // StyleColorsExample defines a few choice color options. Use this is just // as an example to customize the Tracker/text colors. StyleColorsExample = StyleColors{ Message: text.Colors{text.FgWhite}, Error: text.Colors{text.FgRed}, Percent: text.Colors{text.FgHiRed}, Stats: text.Colors{text.FgHiBlack}, Time: text.Colors{text.FgGreen}, Tracker: text.Colors{text.FgYellow}, Value: text.Colors{text.FgCyan}, } ) // StyleOptions defines misc. options to control how the Tracker or its parts // gets rendered. type StyleOptions struct { DoneString string // "done!" string ErrorString string // "error!" string ETAPrecision time.Duration // precision for ETA ETAString string // string for ETA Separator string // text between message and tracker SnipIndicator string // text denoting message snipping PercentFormat string // formatting to use for percentage PercentIndeterminate string // when percentage cannot be computed TimeDonePrecision time.Duration // precision for time when done TimeInProgressPrecision time.Duration // precision for time when in progress TimeOverallPrecision time.Duration // precision for overall time } var ( // StyleOptionsDefault defines sane defaults for the Options. Use this as an // example to customize the Tracker rendering. StyleOptionsDefault = StyleOptions{ DoneString: "done!", ErrorString: "fail!", ETAPrecision: time.Second, ETAString: "~ETA", PercentFormat: "%5.2f%%", PercentIndeterminate: " ??? ", Separator: " ... ", SnipIndicator: "~", TimeDonePrecision: time.Millisecond, TimeInProgressPrecision: time.Microsecond, TimeOverallPrecision: time.Second, } ) go-pretty-6.2.4/progress/tracker.go000066400000000000000000000117521407250454200173140ustar00rootroot00000000000000package progress import ( "math" "sync" "time" ) // Tracker helps track the progress of a single task. The way to use it is to // instantiate a Tracker with a valid Message, a valid (expected) Total, and // Units values. This should then be fed to the Progress Writer with the // Writer.AppendTracker() method. When the task that is being done has progress, // increment the value using the Tracker.Increment(value) method. type Tracker struct { // Message should contain a short description of the "task"; please note // that this should NOT be updated in the middle of progress - you should // instead use UpdateMessage() to do this safely without hitting any race // conditions Message string // ExpectedDuration tells how long this task is expected to take; and will // be used in calculation of the ETA value ExpectedDuration time.Duration // Total should be set to the (expected) Total/Final value to be reached Total int64 // Units defines the type of the "value" being tracked Units Units done bool err bool mutex sync.RWMutex timeStart time.Time timeStop time.Time value int64 } // ETA returns the expected time of "arrival" or completion of this tracker. It // is an estimate and is not guaranteed. func (t *Tracker) ETA() time.Duration { t.mutex.RLock() defer t.mutex.RUnlock() timeTaken := time.Since(t.timeStart) if t.ExpectedDuration > time.Duration(0) && t.ExpectedDuration > timeTaken { return t.ExpectedDuration - timeTaken } pDone := int64(t.percentDoneWithoutLock()) if pDone == 0 { return time.Duration(0) } return time.Duration((int64(timeTaken) / pDone) * (100 - pDone)) } // Increment updates the current value of the task being tracked. func (t *Tracker) Increment(value int64) { t.mutex.Lock() t.incrementWithoutLock(value) t.mutex.Unlock() } // IncrementWithError updates the current value of the task being tracked and // marks that an error occurred. func (t *Tracker) IncrementWithError(value int64) { t.mutex.Lock() t.incrementWithoutLock(value) t.err = true t.mutex.Unlock() } // IsDone returns true if the tracker is done (value has reached the expected // Total set during initialization). func (t *Tracker) IsDone() bool { t.mutex.RLock() defer t.mutex.RUnlock() return t.done } // IsErrored true if an error was set with IncrementWithError or MarkAsErrored. func (t *Tracker) IsErrored() bool { t.mutex.RLock() defer t.mutex.RUnlock() return t.err } // IsIndeterminate returns true if the tracker is indeterminate; i.e., the total // is unknown and it is impossible to auto-calculate if tracking is done. func (t *Tracker) IsIndeterminate() bool { t.mutex.RLock() defer t.mutex.RUnlock() return t.Total == 0 } // MarkAsDone forces completion of the tracker by updating the current value as // the expected Total value. func (t *Tracker) MarkAsDone() { t.mutex.Lock() t.Total = t.value t.stop() t.mutex.Unlock() } // MarkAsErrored forces completion of the tracker by updating the current value as // the expected Total value, and recording as error. func (t *Tracker) MarkAsErrored() { t.mutex.Lock() // only update error if not done and if not previously set if !t.done { t.Total = t.value t.err = true t.stop() } t.mutex.Unlock() } // PercentDone returns the currently completed percentage value. func (t *Tracker) PercentDone() float64 { t.mutex.RLock() defer t.mutex.RUnlock() return t.percentDoneWithoutLock() } func (t *Tracker) percentDoneWithoutLock() float64 { if t.Total == 0 { return 0 } return float64(t.value) * 100.0 / float64(t.Total) } // Reset resets the tracker to its initial state. func (t *Tracker) Reset() { t.mutex.Lock() t.done = false t.err = false t.timeStart = time.Time{} t.timeStop = time.Time{} t.value = 0 t.mutex.Unlock() } // SetValue sets the value of the tracker and re-calculates if the tracker is // "done". func (t *Tracker) SetValue(value int64) { t.mutex.Lock() t.done = false t.timeStop = time.Time{} t.value = 0 t.incrementWithoutLock(value) t.mutex.Unlock() } // UpdateMessage updates the message string. func (t *Tracker) UpdateMessage(msg string) { t.mutex.Lock() t.Message = msg t.mutex.Unlock() } // Value returns the current value of the tracker. func (t *Tracker) Value() int64 { t.mutex.RLock() defer t.mutex.RUnlock() return t.value } func (t *Tracker) message() string { t.mutex.RLock() defer t.mutex.RUnlock() return t.Message } func (t *Tracker) valueAndTotal() (int64, int64) { t.mutex.RLock() value := t.value total := t.Total t.mutex.RUnlock() return value, total } func (t *Tracker) incrementWithoutLock(value int64) { if !t.done { t.value += value if t.Total > 0 && t.value >= t.Total { t.stop() } } } func (t *Tracker) start() { t.mutex.Lock() if t.Total < 0 { t.Total = math.MaxInt64 } t.done = false t.err = false t.timeStart = time.Now() t.mutex.Unlock() } // this must be called with the mutex held with a write lock func (t *Tracker) stop() { t.done = true t.timeStop = time.Now() if t.value > t.Total { t.Total = t.value } } go-pretty-6.2.4/progress/tracker_sort.go000066400000000000000000000053621407250454200203630ustar00rootroot00000000000000package progress import "sort" // SortBy helps sort a list of Trackers by various means. type SortBy int const ( // SortByNone doesn't do any sorting == sort by insertion order. SortByNone SortBy = iota // SortByMessage sorts by the Message alphabetically in ascending order. SortByMessage // SortByMessageDsc sorts by the Message alphabetically in descending order. SortByMessageDsc // SortByPercent sorts by the Percentage complete in ascending order. SortByPercent // SortByPercentDsc sorts by the Percentage complete in descending order. SortByPercentDsc // SortByValue sorts by the Value in ascending order. SortByValue // SortByValueDsc sorts by the Value in descending order. SortByValueDsc ) // Sort applies the sorting method defined by SortBy. func (sb SortBy) Sort(trackers []*Tracker) { switch sb { case SortByMessage: sort.Sort(sortByMessage(trackers)) case SortByMessageDsc: sort.Sort(sortByMessageDsc(trackers)) case SortByPercent: sort.Sort(sortByPercent(trackers)) case SortByPercentDsc: sort.Sort(sortByPercentDsc(trackers)) case SortByValue: sort.Sort(sortByValue(trackers)) case SortByValueDsc: sort.Sort(sortByValueDsc(trackers)) default: // no sort } } type sortByMessage []*Tracker func (sb sortByMessage) Len() int { return len(sb) } func (sb sortByMessage) Swap(i, j int) { sb[i], sb[j] = sb[j], sb[i] } func (sb sortByMessage) Less(i, j int) bool { return sb[i].message() < sb[j].message() } type sortByMessageDsc []*Tracker func (sb sortByMessageDsc) Len() int { return len(sb) } func (sb sortByMessageDsc) Swap(i, j int) { sb[i], sb[j] = sb[j], sb[i] } func (sb sortByMessageDsc) Less(i, j int) bool { return sb[i].message() > sb[j].message() } type sortByPercent []*Tracker func (sb sortByPercent) Len() int { return len(sb) } func (sb sortByPercent) Swap(i, j int) { sb[i], sb[j] = sb[j], sb[i] } func (sb sortByPercent) Less(i, j int) bool { return sb[i].PercentDone() < sb[j].PercentDone() } type sortByPercentDsc []*Tracker func (sb sortByPercentDsc) Len() int { return len(sb) } func (sb sortByPercentDsc) Swap(i, j int) { sb[i], sb[j] = sb[j], sb[i] } func (sb sortByPercentDsc) Less(i, j int) bool { return sb[i].PercentDone() > sb[j].PercentDone() } type sortByValue []*Tracker func (sb sortByValue) Len() int { return len(sb) } func (sb sortByValue) Swap(i, j int) { sb[i], sb[j] = sb[j], sb[i] } func (sb sortByValue) Less(i, j int) bool { return sb[i].value < sb[j].value } type sortByValueDsc []*Tracker func (sb sortByValueDsc) Len() int { return len(sb) } func (sb sortByValueDsc) Swap(i, j int) { sb[i], sb[j] = sb[j], sb[i] } func (sb sortByValueDsc) Less(i, j int) bool { return sb[i].value > sb[j].value } go-pretty-6.2.4/progress/tracker_sort_test.go000066400000000000000000000034711407250454200214210ustar00rootroot00000000000000package progress import ( "testing" "github.com/stretchr/testify/assert" ) func TestSortBy(t *testing.T) { trackers := []*Tracker{ {Message: "Downloading File # 2", Total: 1000, value: 300}, {Message: "Downloading File # 1", Total: 1000, value: 100}, {Message: "Downloading File # 3", Total: 1000, value: 500}, } SortByNone.Sort(trackers) assert.Equal(t, "Downloading File # 2", trackers[0].Message) assert.Equal(t, "Downloading File # 1", trackers[1].Message) assert.Equal(t, "Downloading File # 3", trackers[2].Message) SortByMessage.Sort(trackers) assert.Equal(t, "Downloading File # 1", trackers[0].Message) assert.Equal(t, "Downloading File # 2", trackers[1].Message) assert.Equal(t, "Downloading File # 3", trackers[2].Message) SortByMessageDsc.Sort(trackers) assert.Equal(t, "Downloading File # 3", trackers[0].Message) assert.Equal(t, "Downloading File # 2", trackers[1].Message) assert.Equal(t, "Downloading File # 1", trackers[2].Message) SortByPercent.Sort(trackers) assert.Equal(t, "Downloading File # 1", trackers[0].Message) assert.Equal(t, "Downloading File # 2", trackers[1].Message) assert.Equal(t, "Downloading File # 3", trackers[2].Message) SortByPercentDsc.Sort(trackers) assert.Equal(t, "Downloading File # 3", trackers[0].Message) assert.Equal(t, "Downloading File # 2", trackers[1].Message) assert.Equal(t, "Downloading File # 1", trackers[2].Message) SortByValue.Sort(trackers) assert.Equal(t, "Downloading File # 1", trackers[0].Message) assert.Equal(t, "Downloading File # 2", trackers[1].Message) assert.Equal(t, "Downloading File # 3", trackers[2].Message) SortByValueDsc.Sort(trackers) assert.Equal(t, "Downloading File # 3", trackers[0].Message) assert.Equal(t, "Downloading File # 2", trackers[1].Message) assert.Equal(t, "Downloading File # 1", trackers[2].Message) } go-pretty-6.2.4/progress/tracker_test.go000066400000000000000000000116031407250454200203460ustar00rootroot00000000000000package progress import ( "testing" "time" "github.com/stretchr/testify/assert" ) func TestTracker_ETA(t *testing.T) { timeDelayUnit := time.Millisecond timeDelay := timeDelayUnit * 25 tracker := Tracker{} assert.Equal(t, time.Duration(0), tracker.ETA()) tracker.Total = 100 tracker.start() assert.Equal(t, time.Duration(0), tracker.ETA()) time.Sleep(timeDelay) tracker.Increment(50) assert.NotEqual(t, time.Duration(0), tracker.ETA()) tracker.Increment(50) assert.Equal(t, time.Duration(0), tracker.ETA()) tracker = Tracker{Total: 100, ExpectedDuration: timeDelay} tracker.start() assert.True(t, tracker.ETA() <= tracker.ExpectedDuration) time.Sleep(timeDelay) tracker.Increment(50) assert.NotEqual(t, time.Duration(0), tracker.ETA()) tracker.Increment(50) assert.Equal(t, time.Duration(0), tracker.ETA()) } func TestTracker_Increment(t *testing.T) { tracker := Tracker{Total: 100} assert.Equal(t, int64(0), tracker.value) assert.Equal(t, int64(100), tracker.Total) tracker.Increment(10) assert.Equal(t, int64(10), tracker.value) assert.Equal(t, int64(100), tracker.Total) tracker.Increment(100) assert.Equal(t, int64(110), tracker.value) assert.Equal(t, int64(110), tracker.Total) assert.False(t, tracker.timeStop.IsZero()) assert.True(t, tracker.IsDone()) } func TestTracker_IncrementWithError(t *testing.T) { tracker := Tracker{Total: 100} assert.Equal(t, int64(0), tracker.value) assert.Equal(t, int64(100), tracker.Total) assert.False(t, tracker.IsErrored()) tracker.IncrementWithError(10) assert.Equal(t, int64(10), tracker.value) assert.Equal(t, int64(100), tracker.Total) assert.True(t, tracker.IsErrored()) tracker.IncrementWithError(100) assert.Equal(t, int64(110), tracker.value) assert.Equal(t, int64(110), tracker.Total) assert.False(t, tracker.timeStop.IsZero()) assert.True(t, tracker.IsErrored()) assert.True(t, tracker.IsDone()) } func TestTracker_IsDone(t *testing.T) { tracker := Tracker{Total: 10} assert.False(t, tracker.IsDone()) tracker.Increment(10) assert.True(t, tracker.IsDone()) } func TestTracker_IsIndeterminate(t *testing.T) { tracker := Tracker{Total: 10} assert.False(t, tracker.IsIndeterminate()) tracker.Total = 0 assert.True(t, tracker.IsIndeterminate()) } func TestTracker_MarkAsDone(t *testing.T) { tracker := Tracker{} assert.False(t, tracker.IsDone()) assert.False(t, tracker.IsErrored()) assert.True(t, tracker.timeStop.IsZero()) tracker.MarkAsDone() assert.True(t, tracker.IsDone()) assert.False(t, tracker.IsErrored()) assert.False(t, tracker.timeStop.IsZero()) tracker.MarkAsErrored() assert.True(t, tracker.IsDone()) assert.False(t, tracker.IsErrored()) assert.False(t, tracker.timeStop.IsZero()) } func TestTracker_MarkAsError(t *testing.T) { tracker := Tracker{} assert.False(t, tracker.IsDone()) assert.False(t, tracker.IsErrored()) assert.True(t, tracker.timeStop.IsZero()) tracker.MarkAsErrored() assert.True(t, tracker.IsDone()) assert.True(t, tracker.IsErrored()) assert.False(t, tracker.timeStop.IsZero()) tracker.MarkAsDone() assert.True(t, tracker.IsDone()) assert.True(t, tracker.IsErrored()) assert.False(t, tracker.timeStop.IsZero()) } func TestTracker_PercentDone(t *testing.T) { tracker := Tracker{} assert.Equal(t, 0.00, tracker.PercentDone()) tracker.Total = 100 assert.Equal(t, 0.00, tracker.PercentDone()) for idx := 1; idx <= 100; idx++ { tracker.Increment(1) assert.Equal(t, float64(idx), tracker.PercentDone()) } } func TestTracker_Reset(t *testing.T) { tracker := Tracker{Total: 100} assert.False(t, tracker.done) assert.Equal(t, time.Time{}, tracker.timeStart) assert.Equal(t, time.Time{}, tracker.timeStop) assert.Equal(t, int64(0), tracker.value) tracker.start() tracker.Increment(tracker.Total) tracker.stop() assert.True(t, tracker.done) assert.NotEqual(t, time.Time{}, tracker.timeStart) assert.NotEqual(t, time.Time{}, tracker.timeStop) assert.Equal(t, tracker.Total, tracker.value) tracker.Reset() assert.False(t, tracker.done) assert.Equal(t, time.Time{}, tracker.timeStart) assert.Equal(t, time.Time{}, tracker.timeStop) assert.Equal(t, int64(0), tracker.value) } func TestTracker_SetValue(t *testing.T) { tracker := Tracker{Total: 100} assert.Equal(t, int64(0), tracker.value) assert.False(t, tracker.done) tracker.SetValue(5) assert.Equal(t, int64(5), tracker.value) assert.False(t, tracker.done) tracker.SetValue(tracker.Total) assert.Equal(t, tracker.Total, tracker.value) assert.True(t, tracker.done) } func TestTracker_Value(t *testing.T) { tracker := Tracker{} assert.Equal(t, int64(0), tracker.value) assert.Equal(t, int64(0), tracker.Value()) tracker.SetValue(5) assert.Equal(t, int64(5), tracker.value) assert.Equal(t, int64(5), tracker.Value()) } func TestTracker_UpdateMessage(t *testing.T) { tracker := Tracker{Message: "foo"} assert.Equal(t, "foo", tracker.message()) tracker.UpdateMessage("bar") assert.Equal(t, "bar", tracker.message()) } go-pretty-6.2.4/progress/units.go000066400000000000000000000052151407250454200170200ustar00rootroot00000000000000package progress import ( "fmt" ) // Units defines the "type" of the value being tracked by the Tracker. type Units struct { Notation string Formatter func(value int64) string } var ( // UnitsDefault doesn't define any units. The value will be treated as any // other number. UnitsDefault = Units{ Notation: "", Formatter: FormatNumber, } // UnitsBytes defines the value as a storage unit. Values will be converted // and printed in one of these forms: B, KB, MB, GB, TB, PB UnitsBytes = Units{ Notation: "", Formatter: FormatBytes, } // UnitsCurrencyDollar defines the value as a Dollar amount. Values will be // converted and printed in one of these forms: $x.yz, $x.yzK, $x.yzM, // $x.yzB, $x.yzT UnitsCurrencyDollar = Units{ Notation: "$", Formatter: FormatNumber, } // UnitsCurrencyEuro defines the value as a Euro amount. Values will be // converted and printed in one of these forms: ₠x.yz, ₠x.yzK, ₠x.yzM, // ₠x.yzB, ₠x.yzT UnitsCurrencyEuro = Units{ Notation: "₠", Formatter: FormatNumber, } // UnitsCurrencyPound defines the value as a Pound amount. Values will be // converted and printed in one of these forms: £x.yz, £x.yzK, £x.yzM, // £x.yzB, £x.yzT UnitsCurrencyPound = Units{ Notation: "£", Formatter: FormatNumber, } ) // Sprint prints the value as defined by the Units. func (tu Units) Sprint(value int64) string { if tu.Formatter == nil { return tu.Notation + FormatNumber(value) } return tu.Notation + tu.Formatter(value) } // FormatBytes formats the given value as a "Byte". func FormatBytes(value int64) string { if value < 1000 { return fmt.Sprintf("%dB", value) } else if value < 1000000 { return fmt.Sprintf("%.2fKB", float64(value)/1000.0) } else if value < 1000000000 { return fmt.Sprintf("%.2fMB", float64(value)/1000000.0) } else if value < 1000000000000 { return fmt.Sprintf("%.2fGB", float64(value)/1000000000.0) } else if value < 1000000000000000 { return fmt.Sprintf("%.2fTB", float64(value)/1000000000000.0) } return fmt.Sprintf("%.2fPB", float64(value)/1000000000000000.0) } // FormatNumber formats the given value as a "regular number". func FormatNumber(value int64) string { if value < 1000 { return fmt.Sprintf("%d", value) } else if value < 1000000 { return fmt.Sprintf("%.2fK", float64(value)/1000.0) } else if value < 1000000000 { return fmt.Sprintf("%.2fM", float64(value)/1000000.0) } else if value < 1000000000000 { return fmt.Sprintf("%.2fB", float64(value)/1000000000.0) } else if value < 1000000000000000 { return fmt.Sprintf("%.2fT", float64(value)/1000000000000.0) } return fmt.Sprintf("%.2fQ", float64(value)/1000000000000000.0) } go-pretty-6.2.4/progress/units_test.go000066400000000000000000000024321407250454200200550ustar00rootroot00000000000000package progress import ( "testing" "github.com/stretchr/testify/assert" ) func TestFormatBytes(t *testing.T) { assert.Equal(t, "1B", FormatBytes(1)) assert.Equal(t, "1.50KB", FormatBytes(1500)) assert.Equal(t, "1.50MB", FormatBytes(1500000)) assert.Equal(t, "1.50GB", FormatBytes(1500000000)) assert.Equal(t, "1.50TB", FormatBytes(1500000000000)) assert.Equal(t, "1.50PB", FormatBytes(1500000000000000)) assert.Equal(t, "1500.00PB", FormatBytes(1500000000000000000)) } func TestFormatNumber(t *testing.T) { assert.Equal(t, "1", FormatNumber(1)) assert.Equal(t, "1.50K", FormatNumber(1500)) assert.Equal(t, "1.50M", FormatNumber(1500000)) assert.Equal(t, "1.50B", FormatNumber(1500000000)) assert.Equal(t, "1.50T", FormatNumber(1500000000000)) assert.Equal(t, "1.50Q", FormatNumber(1500000000000000)) assert.Equal(t, "1500.00Q", FormatNumber(1500000000000000000)) } func TestUnits_Sprint(t *testing.T) { assert.Equal(t, "1.50K", UnitsDefault.Sprint(1500)) assert.Equal(t, "1.50KB", UnitsBytes.Sprint(1500)) assert.Equal(t, "$1.50K", UnitsCurrencyDollar.Sprint(1500)) assert.Equal(t, "₠1.50K", UnitsCurrencyEuro.Sprint(1500)) assert.Equal(t, "£1.50K", UnitsCurrencyPound.Sprint(1500)) customUnits := Units{Notation: "#"} assert.Equal(t, "#1.50K", customUnits.Sprint(1500)) } go-pretty-6.2.4/progress/writer.go000066400000000000000000000016421407250454200171720ustar00rootroot00000000000000package progress import ( "io" "time" ) // Writer declares the interfaces that can be used to setup and render a // Progress tracker with one or more trackers. type Writer interface { AppendTracker(tracker *Tracker) AppendTrackers(trackers []*Tracker) IsRenderInProgress() bool Length() int LengthActive() int LengthDone() int LengthInQueue() int SetAutoStop(autoStop bool) SetMessageWidth(width int) SetNumTrackersExpected(numTrackers int) SetOutputWriter(output io.Writer) SetSortBy(sortBy SortBy) SetStyle(style Style) SetTrackerLength(length int) SetTrackerPosition(position Position) ShowETA(show bool) ShowOverallTracker(show bool) ShowPercentage(show bool) ShowTime(show bool) ShowTracker(show bool) ShowValue(show bool) SetUpdateFrequency(frequency time.Duration) Stop() Style() *Style Render() } // NewWriter initializes and returns a Writer. func NewWriter() Writer { return &Progress{} } go-pretty-6.2.4/table/000077500000000000000000000000001407250454200145475ustar00rootroot00000000000000go-pretty-6.2.4/table/README.md000066400000000000000000000435711407250454200160400ustar00rootroot00000000000000# Table [![Go Reference](https://pkg.go.dev/badge/github.com/jedib0t/go-pretty/v6/table.svg)](https://pkg.go.dev/github.com/jedib0t/go-pretty/v6/table) Pretty-print tables into ASCII/Unicode strings. - Add Rows one-by-one or as a group (`AppendRow`/`AppendRows`) - Add Header(s) and Footer(s) (`AppendHeader`/`AppendFooter`) - Add a Separator manually after any Row (`AppendSeparator`) - Auto Index Rows (1, 2, 3 ...) and Columns (A, B, C, ...) (`SetAutoIndex`) - Auto Merge - Cells in a Row (`RowConfig.AutoMerge`) - Columns (`ColumnConfig.AutoMerge`) - Limit the length of - Rows (`SetAllowedRowLength`) - Columns (`ColumnConfig.Width*`) - Page results by a specified number of Lines (`SetPageSize`) - Alignment - Horizontal & Vertical - Auto (horizontal) Align (numeric columns aligned Right) - Custom (horizontal) Align per column (`ColumnConfig.Align*`) - Custom (vertical) VAlign per column with multi-line cell support (`ColumnConfig.VAlign*`) - Mirror output to an `io.Writer` (ex. `os.StdOut`) (`SetOutputMirror`) - Sort by one or more Columns (`SortBy`) - Suppress/hide columns with no content (`SuppressEmptyColumns`) - Customizable Cell rendering per Column (`ColumnConfig.Transformer*`) - Hide any columns that you don't want displayed (`ColumnConfig.Hidden`) - Reset Headers/Rows/Footers at will to reuse the same Table Writer (`Reset*`) - Completely customizable styles (`SetStyle`/`Style`) - Many ready-to-use styles: [style.go](style.go) - Colorize Headers/Body/Footers using [../text/color.go](../text/color.go) - Custom text-case for Headers/Body/Footers - Enable separators between each row - Render table without a Border - and a lot more... - Render as: - (ASCII/Unicode) Table - CSV - HTML Table (with custom CSS Class) - Markdown Table ``` +---------------------------------------------------------------------+ | Game of Thrones + +-----+------------+-----------+--------+-----------------------------+ | # | FIRST NAME | LAST NAME | SALARY | | +-----+------------+-----------+--------+-----------------------------+ | 1 | Arya | Stark | 3000 | | | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | | 300 | Tyrion | Lannister | 5000 | | +-----+------------+-----------+--------+-----------------------------+ | | | TOTAL | 10000 | | +-----+------------+-----------+--------+-----------------------------+ ``` A demonstration of all the capabilities can be found here: [../cmd/demo-table](../cmd/demo-table) If you want very specific examples, read ahead. # Examples All the examples below are going to start with the following block, although nothing except a single Row is mandatory for the `Render()` function to render something: ```golang package main import ( "os" "github.com/jedib0t/go-pretty/v6/table" ) func main() { t := table.NewWriter() t.SetOutputMirror(os.Stdout) t.AppendHeader(table.Row{"#", "First Name", "Last Name", "Salary"}) t.AppendRows([]table.Row{ {1, "Arya", "Stark", 3000}, {20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"}, }) t.AppendSeparator() t.AppendRow([]interface{}{300, "Tyrion", "Lannister", 5000}) t.AppendFooter(table.Row{"", "", "Total", 10000}) t.Render() } ``` Running the above will result in: ``` +-----+------------+-----------+--------+-----------------------------+ | # | FIRST NAME | LAST NAME | SALARY | | +-----+------------+-----------+--------+-----------------------------+ | 1 | Arya | Stark | 3000 | | | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | +-----+------------+-----------+--------+-----------------------------+ | 300 | Tyrion | Lannister | 5000 | | +-----+------------+-----------+--------+-----------------------------+ | | | TOTAL | 10000 | | +-----+------------+-----------+--------+-----------------------------+ ``` ## Styles You can customize almost every single thing about the table above. The previous example just defaulted to `StyleDefault` during `Render()`. You can use a ready-to-use style (as in [style.go](style.go)) or customize it as you want. ### Ready-to-use Styles Table comes with a bunch of ready-to-use Styles that make the table look really good. Set or Change the style using: ```golang t.SetStyle(table.StyleLight) t.Render() ``` to get: ``` ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐ │ # │ FIRST NAME │ LAST NAME │ SALARY │ │ ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ │ 1 │ Arya │ Stark │ 3000 │ │ │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ │ 300 │ Tyrion │ Lannister │ 5000 │ │ ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ │ │ │ TOTAL │ 10000 │ │ └─────┴────────────┴───────────┴────────┴─────────────────────────────┘ ``` Or if you want to use a full-color mode, and don't care for boxes, use: ```golang t.SetStyle(table.StyleColoredBright) t.Render() ``` to get: Colored Table ### Roll your own Style You can also roll your own style: ```golang t.SetStyle(table.Style{ Name: "myNewStyle", Box: table.BoxStyle{ BottomLeft: "\\", BottomRight: "/", BottomSeparator: "v", Left: "[", LeftSeparator: "{", MiddleHorizontal: "-", MiddleSeparator: "+", MiddleVertical: "|", PaddingLeft: "<", PaddingRight: ">", Right: "]", RightSeparator: "}", TopLeft: "(", TopRight: ")", TopSeparator: "^", UnfinishedRow: " ~~~", }, Color: table.ColorOptions{ IndexColumn: text.Colors{text.BgCyan, text.FgBlack}, Footer: text.Colors{text.BgCyan, text.FgBlack}, Header: text.Colors{text.BgHiCyan, text.FgBlack}, Row: text.Colors{text.BgHiWhite, text.FgBlack}, RowAlternate: text.Colors{text.BgWhite, text.FgBlack}, }, Format: table.FormatOptions{ Footer: text.FormatUpper, Header: text.FormatUpper, Row: text.FormatDefault, }, Options: table.Options{ DrawBorder: true, SeparateColumns: true, SeparateFooter: true, SeparateHeader: true, SeparateRows: false, }, }) ``` Or you can use one of the ready-to-use Styles, and just make a few tweaks: ```golang t.SetStyle(table.StyleLight) t.Style().Color.Header = text.Colors{text.BgHiCyan, text.FgBlack} t.Style().Color.IndexColumn = text.Colors{text.BgHiCyan, text.FgBlack} t.Style().Format.Footer = text.FormatLower t.Style().Options.DrawBorder = false ``` ## Auto-Merge You can auto-merge cells horizontally and vertically, but you have request for it specifically for each row/column using `RowConfig` or `ColumnConfig`. ```golang rowConfigAutoMerge := table.RowConfig{AutoMerge: true} t := table.NewWriter() t.AppendHeader(table.Row{"Node IP", "Pods", "Namespace", "Container", "RCE", "RCE"}, rowConfigAutoMerge) t.AppendHeader(table.Row{"", "", "", "", "EXE", "RUN"}) t.AppendRow(table.Row{"1.1.1.1", "Pod 1A", "NS 1A", "C 1", "Y", "Y"}, rowConfigAutoMerge) t.AppendRow(table.Row{"1.1.1.1", "Pod 1A", "NS 1A", "C 2", "Y", "N"}, rowConfigAutoMerge) t.AppendRow(table.Row{"1.1.1.1", "Pod 1A", "NS 1B", "C 3", "N", "N"}, rowConfigAutoMerge) t.AppendRow(table.Row{"1.1.1.1", "Pod 1B", "NS 2", "C 4", "N", "N"}, rowConfigAutoMerge) t.AppendRow(table.Row{"1.1.1.1", "Pod 1B", "NS 2", "C 5", "Y", "N"}, rowConfigAutoMerge) t.AppendRow(table.Row{"2.2.2.2", "Pod 2", "NS 3", "C 6", "Y", "Y"}, rowConfigAutoMerge) t.AppendRow(table.Row{"2.2.2.2", "Pod 2", "NS 3", "C 7", "Y", "Y"}, rowConfigAutoMerge) t.AppendFooter(table.Row{"", "", "", 7, 5, 3}) t.SetAutoIndex(true) t.SetColumnConfigs([]table.ColumnConfig{ {Number: 1, AutoMerge: true}, {Number: 2, AutoMerge: true}, {Number: 3, AutoMerge: true}, {Number: 4, AutoMerge: true}, {Number: 5, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter}, {Number: 6, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter}, }) t.SetOutputMirror(os.Stdout) t.SetStyle(table.StyleLight) t.Style().Options.SeparateRows = true fmt.Println(t.Render()) ``` to get: ``` ┌───┬─────────┬────────┬───────────┬───────────┬───────────┐ │ │ NODE IP │ PODS │ NAMESPACE │ CONTAINER │ RCE │ │ │ │ │ │ ├─────┬─────┤ │ │ │ │ │ │ EXE │ RUN │ ├───┼─────────┼────────┼───────────┼───────────┼─────┴─────┤ │ 1 │ 1.1.1.1 │ Pod 1A │ NS 1A │ C 1 │ Y │ ├───┤ │ │ ├───────────┼─────┬─────┤ │ 2 │ │ │ │ C 2 │ Y │ N │ ├───┤ │ ├───────────┼───────────┼─────┴─────┤ │ 3 │ │ │ NS 1B │ C 3 │ N │ ├───┤ ├────────┼───────────┼───────────┼───────────┤ │ 4 │ │ Pod 1B │ NS 2 │ C 4 │ N │ ├───┤ │ │ ├───────────┼─────┬─────┤ │ 5 │ │ │ │ C 5 │ Y │ N │ ├───┼─────────┼────────┼───────────┼───────────┼─────┴─────┤ │ 6 │ 2.2.2.2 │ Pod 2 │ NS 3 │ C 6 │ Y │ ├───┤ │ │ ├───────────┼───────────┤ │ 7 │ │ │ │ C 7 │ Y │ ├───┼─────────┼────────┼───────────┼───────────┼─────┬─────┤ │ │ │ │ │ 7 │ 5 │ 3 │ └───┴─────────┴────────┴───────────┴───────────┴─────┴─────┘ ``` ## Paging You can limit then number of lines rendered in a single "Page". This logic can handle rows with multiple lines too. Here is a simple example: ```golang t.SetPageSize(1) t.Render() ``` to get: ``` +-----+------------+-----------+--------+-----------------------------+ | # | FIRST NAME | LAST NAME | SALARY | | +-----+------------+-----------+--------+-----------------------------+ | 1 | Arya | Stark | 3000 | | +-----+------------+-----------+--------+-----------------------------+ | | | TOTAL | 10000 | | +-----+------------+-----------+--------+-----------------------------+ +-----+------------+-----------+--------+-----------------------------+ | # | FIRST NAME | LAST NAME | SALARY | | +-----+------------+-----------+--------+-----------------------------+ | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | +-----+------------+-----------+--------+-----------------------------+ | | | TOTAL | 10000 | | +-----+------------+-----------+--------+-----------------------------+ +-----+------------+-----------+--------+-----------------------------+ | # | FIRST NAME | LAST NAME | SALARY | | +-----+------------+-----------+--------+-----------------------------+ | 300 | Tyrion | Lannister | 5000 | | +-----+------------+-----------+--------+-----------------------------+ | | | TOTAL | 10000 | | +-----+------------+-----------+--------+-----------------------------+ ``` ## Sorting Sorting can be done on one or more columns. The following code will make the rows be sorted first by "First Name" and then by "Last Name" (in case of similar "First Name" entries). ```golang t.SortBy([]table.SortBy{ {Name: "First Name", Mode: table.Asc}, {Name: "Last Name", Mode: table.Asc}, }) ``` ## Wrapping (or) Row/Column Width restrictions You can restrict the maximum (text) width for a Row: ```golang t.SetAllowedRowLength(50) t.Render() ``` to get: ``` +-----+------------+-----------+--------+------- ~ | # | FIRST NAME | LAST NAME | SALARY | ~ +-----+------------+-----------+--------+------- ~ | 1 | Arya | Stark | 3000 | ~ | 20 | Jon | Snow | 2000 | You kn ~ +-----+------------+-----------+--------+------- ~ | 300 | Tyrion | Lannister | 5000 | ~ +-----+------------+-----------+--------+------- ~ | | | TOTAL | 10000 | ~ +-----+------------+-----------+--------+------- ~ ``` ## Column Control - Alignment, Colors, Width and more You can control a lot of things about individual cells/columns which overrides global properties/styles using the `SetColumnConfig()` interface: - Alignment (horizontal & vertical) - Colorization - Transform individual cells based on the content - Visibility - Width (minimum & maximum) ```golang nameTransformer := text.Transformer(func(val interface{}) string { return text.Bold.Sprint(val) }) t.SetColumnConfigs([]ColumnConfig{ { Name: "First Name", Align: text.AlignLeft, AlignFooter: text.AlignLeft, AlignHeader: text.AlignLeft, Colors: text.Colors{text.BgBlack, text.FgRed}, ColorsHeader: text.Colors{text.BgRed, text.FgBlack, text.Bold}, ColorsFooter: text.Colors{text.BgRed, text.FgBlack}, Hidden: false, Transformer: nameTransformer, TransformerFooter: nameTransformer, TransformerHeader: nameTransformer, VAlign: text.VAlignMiddle, VAlignFooter: text.VAlignTop, VAlignHeader: text.VAlignBottom, WidthMin: 6, WidthMax: 64, } }) ``` ## Render As ... Tables can be rendered in other common formats such as: ### ... CSV ```golang t.RenderCSV() ``` to get: ``` ,First Name,Last Name,Salary, 1,Arya,Stark,3000, 20,Jon,Snow,2000,"You know nothing\, Jon Snow!" 300,Tyrion,Lannister,5000, ,,Total,10000, ``` ### ... HTML Table ```golang t.Style().HTML = table.HTMLOptions{ CSSClass: "game-of-thrones", EmptyColumn: " ", EscapeText: true, Newline: "
          ", } t.RenderHTML() ``` to get: ```html
          # First Name Last Name Salary  
          1 Arya Stark 3000  
          20 Jon Snow 2000 You know nothing, Jon Snow!
          300 Tyrion Lannister 5000  
              Total 10000  
          ``` ### ... Markdown Table ```golang t.RenderMarkdown() ``` to get: ```markdown | # | First Name | Last Name | Salary | | | ---:| --- | --- | ---:| --- | | 1 | Arya | Stark | 3000 | | | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | | 300 | Tyrion | Lannister | 5000 | | | | | Total | 10000 | | ``` go-pretty-6.2.4/table/config.go000066400000000000000000000063321407250454200163470ustar00rootroot00000000000000package table import ( "github.com/jedib0t/go-pretty/v6/text" ) // ColumnConfig contains configurations that determine and modify the way the // contents of the column get rendered. type ColumnConfig struct { // Name is the name of the Column as it appears in the first Header row. // If a Header is not provided, or the name is not found in the header, this // will not work. Name string // Number is the Column # from left. When specified, it overrides the Name // property. If you know the exact Column number, use this instead of Name. Number int // Align defines the horizontal alignment Align text.Align // AlignFooter defines the horizontal alignment of Footer rows AlignFooter text.Align // AlignHeader defines the horizontal alignment of Header rows AlignHeader text.Align // AutoMerge merges cells with similar values and prevents separators from // being drawn. Caveats: // * VAlign is applied on the individual cell and not on the merged cell // * Does not work in CSV/HTML/Markdown render modes // * Does not work well with horizontal auto-merge (RowConfig.AutoMerge) // // Works best when: // * Style().Options.SeparateRows == true // * Style().Color.Row == Style().Color.RowAlternate (or not set) AutoMerge bool // Colors defines the colors to be used on the column Colors text.Colors // ColorsFooter defines the colors to be used on the column in Footer rows ColorsFooter text.Colors // ColorsHeader defines the colors to be used on the column in Header rows ColorsHeader text.Colors // Hidden when set to true will prevent the column from being rendered. // This is useful in cases like needing a column for sorting, but not for // display. Hidden bool // Transformer is a custom-function that changes the way the value gets // rendered to the console. Refer to text/transformer.go for ready-to-use // Transformer functions. Transformer text.Transformer // TransformerFooter is like Transformer but for Footer rows TransformerFooter text.Transformer // TransformerHeader is like Transformer but for Header rows TransformerHeader text.Transformer // VAlign defines the vertical alignment VAlign text.VAlign // VAlignFooter defines the vertical alignment in Footer rows VAlignFooter text.VAlign // VAlignHeader defines the vertical alignment in Header rows VAlignHeader text.VAlign // WidthMax defines the maximum character length of the column WidthMax int // WidthEnforcer enforces the WidthMax value on the column contents; // default: text.WrapText WidthMaxEnforcer WidthEnforcer // WidthMin defines the minimum character length of the column WidthMin int } func (c ColumnConfig) getWidthMaxEnforcer() WidthEnforcer { if c.WidthMax == 0 { return widthEnforcerNone } if c.WidthMaxEnforcer != nil { return c.WidthMaxEnforcer } return text.WrapText } // RowConfig contains configurations that determine and modify the way the // contents of a row get rendered. type RowConfig struct { // AutoMerge merges cells with similar values and prevents separators from // being drawn. Caveats: // * Align is overridden to text.AlignCenter on the merged cell // * Does not work in CSV/HTML/Markdown render modes // * Does not work well with vertical auto-merge (ColumnConfig.AutoMerge) AutoMerge bool } go-pretty-6.2.4/table/config_test.go000066400000000000000000000046651407250454200174150ustar00rootroot00000000000000package table import ( "testing" "github.com/jedib0t/go-pretty/v6/text" "github.com/stretchr/testify/assert" ) func TestColumnConfig_getWidthMaxEnforcer(t *testing.T) { t.Run("no width enforcer", func(t *testing.T) { cc := ColumnConfig{} widthEnforcer := cc.getWidthMaxEnforcer() assert.Equal(t, "1234567890", widthEnforcer("1234567890", 0)) assert.Equal(t, "1234567890", widthEnforcer("1234567890", 1)) assert.Equal(t, "1234567890", widthEnforcer("1234567890", 5)) assert.Equal(t, "1234567890", widthEnforcer("1234567890", 10)) assert.Equal(t, "1234567890", widthEnforcer("1234567890", 100)) assert.Equal(t, "1234567890", widthEnforcer("1234567890", 1000)) }) t.Run("default width enforcer", func(t *testing.T) { cc := ColumnConfig{ WidthMax: 10, } widthEnforcer := cc.getWidthMaxEnforcer() assert.Equal(t, "", widthEnforcer("1234567890", 0)) assert.Equal(t, "1\n2\n3\n4\n5\n6\n7\n8\n9\n0", widthEnforcer("1234567890", 1)) assert.Equal(t, "12345\n67890", widthEnforcer("1234567890", 5)) assert.Equal(t, "1234567890", widthEnforcer("1234567890", 10)) assert.Equal(t, "1234567890", widthEnforcer("1234567890", 100)) assert.Equal(t, "1234567890", widthEnforcer("1234567890", 1000)) }) t.Run("custom width enforcer (1)", func(t *testing.T) { cc := ColumnConfig{ WidthMax: 10, WidthMaxEnforcer: text.Trim, } widthEnforcer := cc.getWidthMaxEnforcer() assert.Equal(t, text.Trim("1234567890", 0), widthEnforcer("1234567890", 0)) assert.Equal(t, text.Trim("1234567890", 1), widthEnforcer("1234567890", 1)) assert.Equal(t, text.Trim("1234567890", 5), widthEnforcer("1234567890", 5)) assert.Equal(t, text.Trim("1234567890", 10), widthEnforcer("1234567890", 10)) assert.Equal(t, text.Trim("1234567890", 100), widthEnforcer("1234567890", 100)) assert.Equal(t, text.Trim("1234567890", 1000), widthEnforcer("1234567890", 1000)) }) t.Run("custom width enforcer (2)", func(t *testing.T) { cc := ColumnConfig{ WidthMax: 10, WidthMaxEnforcer: func(col string, maxLen int) string { return "foo" }, } widthEnforcer := cc.getWidthMaxEnforcer() assert.Equal(t, "foo", widthEnforcer("1234567890", 0)) assert.Equal(t, "foo", widthEnforcer("1234567890", 1)) assert.Equal(t, "foo", widthEnforcer("1234567890", 5)) assert.Equal(t, "foo", widthEnforcer("1234567890", 10)) assert.Equal(t, "foo", widthEnforcer("1234567890", 100)) assert.Equal(t, "foo", widthEnforcer("1234567890", 1000)) }) } go-pretty-6.2.4/table/images/000077500000000000000000000000001407250454200160145ustar00rootroot00000000000000go-pretty-6.2.4/table/images/table-StyleColoredBright.png000066400000000000000000000367421407250454200233730ustar00rootroot00000000000000PNG  IHDRgAMA a cHRMz&u0`:pQ< pHYs%%IR$YiTXtXML:com.adobe.xmp 1 L'Y;IDATxMk$GC/ﭱ3T AcEz̅t b̥< aԽ1,0՘awc%{jp̌ȬȬʬzVW~'3D8 @ @ K9i@ @GA @ L(x@ @( @ d@qE@ @@qD @ @ @wG}*߽%9'OsM>E'OmR~Zp}ah OIښIC~a%.:w|K)ݗrmvJR7jeؚ4S тAR |2S 4}Q*=}J8^MxSeh)&kGُܤ(/˩C"|p:%$-܁CI|r0)5 'G׺;fxν\r[hzӾwW9xǿ#e @GkCQr<./(ElQ6H҅(x[ -ir)s~ZF[J4Ge2k{W'J}iXkyx"gb0rVF瑳l]-% Is(l {;RJs șICߜ8&2?2.~pm9O:2+Ȏg*[Ҩuǿ#H'ýgz K(&Cbm9y"g22r6s申’K{QGәudq$@@j& ZxfJ˫X{70ToUnimy^r{:gֻ_j/9[L9ދ,_fzW{7σwo0"k6~`f)a!@%h&df)/ϙk{VNρ4Jf/΃m99kݐZ|*m+2P+Z94hF m]~ǘqd -4=*`ޣKr4d9Yz/,gFrΚ^Ο(ꗋlFC@k tSq4J+565foai-Vcx-uG V&V oi4׸re>ݠA(O>zw(L<[9+P3Y&eӛ+Y y]+^Z n  dyK-x^^+\=ϫ-'&#^g槓F =B @' M%vJӳMϭt9.@@,/@ @pވ @ 8Vh!@  yI@ @"(%Z@ @Bq^jr@ @ WwEQ- @ F) @ 8V!@ yE@ @B(%j@ @@qj2@ @cphZ^^Vf2@ @`t[q ZZ9CGinQKIPnX)ֲeʩ bx2Mq`66vhbngAGZw@@+(Gl{|nԺu#uW0j0-*hcSwrĵX_}K'î(c8.${ z 8VۘS,%/ۻqwѽ.-K 1&UNSXT,uw"TeMek *̙V[f1 gae>Tk2yɠYʡױzu)jA}&d 8ԏ04SLΛ驛`wd0fq#@c;qD; ۮ0MUiP̠7W!H57K-,ry3_R4T{G:EDed9A⍮<7i ?K8ׁ\U2E@{k"e;\J68 @c%f&\̗1Wf5w捙j)IJ{t:17zbwr</nds3iJ./ՏoUY;>dye<kJDBEUMsdS Z8V94S6۲2~4 ?3դ'A?%a7dO|lco8Ζ/Bw@C9k)Krr_|!K)aC& N^lP=gO2E`Lg4K؄#u3  ؇fS]NB2#^`_̜w~e^:MFځYND es}@=8N nKL+a V[ٛDz<[~ìh*Krld4NY;Tցd'm|O2gѪ ezxD˳l-yp;g!@؈cp:,}S{;[22@ 4@WNcsgk¨g9!v@ @`>8G=zЎv7dk}$sp @h L0fvlD&> @ 0Fg׺9.d @t" @ & ,]dH @MSv @ 8ƫ @ @h7v @ 8ƫ @ @h7v @ 8ƫ @ P7Ғ?;iқSuON߽3DvH#3$h \[Êz~,|-U_CS×/%?f]WO)He ?G3=Ҽ+@ߕhQpeKgߩ<&6<еH?xG<>xen5rkk݃vOpu.הkHO/R2vŵ}}0%*N_gQYYE ^o<{晔g "whZ⸶Atf$ ߪdOd| ]ݺ@\\74xt'EO;C.|L} za>{DnCkJ>i^؋}稵$]vkXEiT|44($SWz`RyDxCWϐKz3/&~zs8)O3Iy-q%vZ.ͼ+xwm'v_aBɅci=a7͞[9+DA-m;\3giq{K3b#7Jt/]nG&ac 4yzX#fYw{lYؾ޷$9{dAWO&frm8s/R @&ն$!SE+uky}a?_w,>.JN**wzt{ރa yxܺ"ۨ }yX>s䢏n)2x3cnp4dҫMye ov'[:Ϯ$j뽺;$˗Xkf^v y$Ѥ xŝeWeh9%!0&u)ʀA1[ʤ*YgIKua%F"?rY$SʵyNm4q0rRw%z.{T4\s*w'd عgdCJ3˗2g}$3ߛ.P6Ș~Nd *> wm[b{"y-}i}c(I-9?TtOyJ*W s<ᵴG ba~V'@ $D&Ž%iQ.9+$&-91o< p. 2Xwb">I'|ߣᚬ(Ϟt}k$ЏA5|iX(\,Xx=i5PNQaݓ{[I~2߃&͜BX}/'xTӥŗg{(gVR*oMu9/;ᵔUۭR u8?E^o6Ww_s϶?T)21T=49vt{COU2Y[INӈ-SܓiÌ味brmd{{sx)̗uk7@=ߕߣV}w\{Ɠjw^q^\o^i\ 7{`;Uq2m,ߗO?ջoK+A5c4zg{>w=FoJq|z 3^? ^&n^N 5H^v,HK*VkY- dAK)VʭOP8Uk ) _,ښ92USqc* [(O΁:4mu:RZίMOi̗=Xbݦ\gΊT>X{f}hZ8>yr>DE= ֠^ 4ދdܱ \б''kz sYK1H1\(#6>}"-VyeIN~tU ])=*D<9Fu'a?Nk)ۻoMi%t=|Jh5B?ZRUMNTBq|*.f=zȔWE[qu-$c:pC6X  x E0zV8Z0\pO7JOsU;ka0JOAJQpS[nQɓ.|=>PC1כ^ "{YrKރy7n^y`JsϚ[>J޸! _B:D?wOF%w=q{gr=tk0u7*oyJ{ unQ˞!H,'b\.7b+@$5pΥ>(rm~xႵ^͠Sj;OO;R{T4\~ M(Oέ1K|g;l_xAxo<|iҍ`Ż9kS D`4iYy,+8@u=MUS)^1kA{YL#oŚ`!lRGUip@8k3WVw:3U yP%aj}W~@7~eq6>z!o3<.T0 ϪO3g찴"mz8uOŧט z!z5j,bڗQʥn )b[iL-/yݰuX\_L?N34]p }Nʔ龬q_o=v;\Qt_hUC Vϝ(x:3Y!?&h%z2U%L8޻wf;b~SÙt5ĺH##,u-J(2qd^ YF|KK"X|Ey5ƾz&(|2q\.r"=LFVMer(p*IJ0&wK6,P0 hXyS_X6j*X`vsI[}c2R+T}=CY!k'mu*Wa <\o߰!]r ,kqnJ3˵1S R^yKfss$@E~WWQˎ]w`^rt^e"/qhrCadgI(0͖g.\_u5T"D @ d fqpj:P2/r]o[+S-ޑ 0Ǚ @).G_|#{ӎr^C[ӭhf @ql:IB @w) @j&s @ @qZ_@ @5@q8A @Ǯ @ P3ǚ @ @qZUѡXVjpTeJ  &%@@ Hδ±)Z]ux6n<=SB 71C3pvEjv@YA{ 9mUŪN6%:^xGݛU7p~M j̬8]pGc5Wˢmg/4h|p'~6W}:o߅sp!PRw @$0ԫjusW @D t鎺gn0:8R hñǢꣿBǑ21N՗̔z[jIk2c.B`2z2ycpwpVME`fűNz.xOo#_[R$G'ru0T'[oa<'Eyc `_@@3fVJ0꘼uT;-!q)E%o屙s61n04>7M|y9g;sNGsSܞ0GHouzNa)!H$bl\^PL)4QPoc.26//ZE zyxgÚ|[;Bק{[s$}ޛ6_Rw6#O P*TGW1g72ܢ~94W-.xkgaٍ8M\/yzi?l&wIe ωV&)˥o 3vw~0p>1SMzmT?&'6Ү?'OhKG?c8֐JmO<7!?X;obY\JQS5gݣdE+u, U*)T͐e'ɉ՗}UhltpLUFf7Hyv';CzXౘ<:5qNA09$) S%nf%~W]"B'P8z7U h{]6N[S*z%w1 o ]`J1AVɬٖ<* 7"`PQH\NF^_ ;e73\%'9 LKnr}J#AD bQœ١^}{wEy|i2Tokw#o Nq̬@/^ y_8e׬1\gvI$v0"Dչ9_:wXlOEGE|0a3ߕ%#vr߇)\b HPSDߕ3. sS?>qoITJ5H+tS #M/uݿ&g)TO] ] B*!X V"UܑF||Ҩ /'Jfǒ38wTRE~烀 %t0xJF{G:BDtgWb P#LUkMRh'ңskPi{hz"q)o{ }iیh~nʁiw0sB`R75zT(L|,qxۗJ'Kc@@PkMR4_-nOC⋢ ~&׾ƾq>K}wJi2)$(2il_Sf"f@yNFhJ(A u҄@ 4s6sQJ<3UJ'i 0 { t.renderTitle(&out) // top-most border t.renderRowsBorderTop(&out) // header rows t.renderRowsHeader(&out) // (data) rows t.renderRows(&out, t.rows, renderHint{}) // footer rows t.renderRowsFooter(&out) // bottom-most border t.renderRowsBorderBottom(&out) // caption if t.caption != "" { out.WriteRune('\n') out.WriteString(t.caption) } } return t.render(&out) } func (t *Table) renderColumn(out *strings.Builder, row rowStr, colIdx int, maxColumnLength int, hint renderHint) int { numColumnsRenderer := 1 // when working on the first column, and autoIndex is true, insert a new // column with the row number on it. if colIdx == 0 && t.autoIndex { hintAutoIndex := hint hintAutoIndex.isAutoIndexColumn = true t.renderColumnAutoIndex(out, hintAutoIndex) } // when working on column number 2 or more, render the column separator if colIdx > 0 { t.renderColumnSeparator(out, row, colIdx, hint) } // extract the text, convert-case if not-empty and align horizontally mergeVertically := t.shouldMergeCellsVertically(colIdx, hint) var colStr string if mergeVertically { // leave colStr empty; align will expand the column as necessary } else if colIdx < len(row) { colStr = t.getFormat(hint).Apply(row[colIdx]) } align := t.getAlign(colIdx, hint) // if horizontal cell merges are enabled, look ahead and see how many cells // have the same content and merge them all until a cell with a different // content is found; override alignment to Center in this case if t.getRowConfig(hint).AutoMerge && !hint.isSeparatorRow { for idx := colIdx + 1; idx < len(row); idx++ { if row[colIdx] != row[idx] { break } align = text.AlignCenter maxColumnLength += t.maxColumnLengths[idx] + text.RuneCount(t.style.Box.PaddingRight+t.style.Box.PaddingLeft) + text.RuneCount(t.style.Box.PaddingRight) numColumnsRenderer++ } } colStr = align.Apply(colStr, maxColumnLength) // pad both sides of the column if !hint.isSeparatorRow || (hint.isSeparatorRow && mergeVertically) { colStr = t.style.Box.PaddingLeft + colStr + t.style.Box.PaddingRight } t.renderColumnColorized(out, colIdx, colStr, hint) return colIdx + numColumnsRenderer } func (t *Table) renderColumnAutoIndex(out *strings.Builder, hint renderHint) { var outAutoIndex strings.Builder outAutoIndex.Grow(t.maxColumnLengths[0]) if hint.isSeparatorRow { numChars := t.autoIndexVIndexMaxLength + utf8.RuneCountInString(t.style.Box.PaddingLeft) + utf8.RuneCountInString(t.style.Box.PaddingRight) chars := t.style.Box.MiddleHorizontal if hint.isAutoIndexColumn && hint.isHeaderOrFooterSeparator() { chars = text.RepeatAndTrim(" ", len(t.style.Box.MiddleHorizontal)) } outAutoIndex.WriteString(text.RepeatAndTrim(chars, numChars)) } else { outAutoIndex.WriteString(t.style.Box.PaddingLeft) rowNumStr := fmt.Sprint(hint.rowNumber) if hint.isHeaderRow || hint.isFooterRow || hint.rowLineNumber > 1 { rowNumStr = strings.Repeat(" ", t.autoIndexVIndexMaxLength) } outAutoIndex.WriteString(text.AlignRight.Apply(rowNumStr, t.autoIndexVIndexMaxLength)) outAutoIndex.WriteString(t.style.Box.PaddingRight) } if t.style.Color.IndexColumn != nil { colors := t.style.Color.IndexColumn if hint.isFooterRow { colors = t.style.Color.Footer } out.WriteString(colors.Sprint(outAutoIndex.String())) } else { out.WriteString(outAutoIndex.String()) } hint.isAutoIndexColumn = true t.renderColumnSeparator(out, rowStr{}, 0, hint) } func (t *Table) renderColumnColorized(out *strings.Builder, colIdx int, colStr string, hint renderHint) { colors := t.getColumnColors(colIdx, hint) if colors != nil { out.WriteString(colors.Sprint(colStr)) } else if hint.isHeaderRow && t.style.Color.Header != nil { out.WriteString(t.style.Color.Header.Sprint(colStr)) } else if hint.isFooterRow && t.style.Color.Footer != nil { out.WriteString(t.style.Color.Footer.Sprint(colStr)) } else if hint.isRegularRow() { if colIdx == t.indexColumn-1 && t.style.Color.IndexColumn != nil { out.WriteString(t.style.Color.IndexColumn.Sprint(colStr)) } else if hint.rowNumber%2 == 0 && t.style.Color.RowAlternate != nil { out.WriteString(t.style.Color.RowAlternate.Sprint(colStr)) } else if t.style.Color.Row != nil { out.WriteString(t.style.Color.Row.Sprint(colStr)) } else { out.WriteString(colStr) } } else { out.WriteString(colStr) } } func (t *Table) renderColumnSeparator(out *strings.Builder, row rowStr, colIdx int, hint renderHint) { if t.style.Options.SeparateColumns { separator := t.getColumnSeparator(row, colIdx, hint) colors := t.getSeparatorColors(hint) if colors.EscapeSeq() != "" { out.WriteString(colors.Sprint(separator)) } else { out.WriteString(separator) } } } func (t *Table) renderLine(out *strings.Builder, row rowStr, hint renderHint) { // if the output has content, it means that this call is working on line // number 2 or more; separate them with a newline if out.Len() > 0 { out.WriteRune('\n') } // use a brand new strings.Builder if a row length limit has been set var outLine *strings.Builder if t.allowedRowLength > 0 { outLine = &strings.Builder{} } else { outLine = out } // grow the strings.Builder to the maximum possible row length outLine.Grow(t.maxRowLength) nextColIdx := 0 t.renderMarginLeft(outLine, hint) for colIdx, maxColumnLength := range t.maxColumnLengths { if colIdx != nextColIdx { continue } nextColIdx = t.renderColumn(outLine, row, colIdx, maxColumnLength, hint) } t.renderMarginRight(outLine, hint) // merge the strings.Builder objects if a new one was created earlier if outLine != out { outLineStr := outLine.String() if text.RuneCount(outLineStr) > t.allowedRowLength { trimLength := t.allowedRowLength - utf8.RuneCountInString(t.style.Box.UnfinishedRow) if trimLength > 0 { out.WriteString(text.Trim(outLineStr, trimLength)) out.WriteString(t.style.Box.UnfinishedRow) } } else { out.WriteString(outLineStr) } } // if a page size has been set, and said number of lines has already // been rendered, and the header is not being rendered right now, render // the header all over again with a spacing line if hint.isRegularRow() { t.numLinesRendered++ if t.pageSize > 0 && t.numLinesRendered%t.pageSize == 0 && !hint.isLastLineOfLastRow() { t.renderRowsFooter(out) t.renderRowsBorderBottom(out) out.WriteString(t.style.Box.PageSeparator) t.renderRowsBorderTop(out) t.renderRowsHeader(out) } } } func (t *Table) renderMarginLeft(out *strings.Builder, hint renderHint) { if t.style.Options.DrawBorder { border := t.style.Box.Left if hint.isBorderTop { if t.title != "" { border = t.style.Box.LeftSeparator } else { border = t.style.Box.TopLeft } } else if hint.isBorderBottom { border = t.style.Box.BottomLeft } else if hint.isSeparatorRow { if t.autoIndex && hint.isHeaderOrFooterSeparator() { border = t.style.Box.Left } else if !t.autoIndex && t.shouldMergeCellsVertically(0, hint) { border = t.style.Box.Left } else { border = t.style.Box.LeftSeparator } } colors := t.getBorderColors(hint) if colors.EscapeSeq() != "" { out.WriteString(colors.Sprint(border)) } else { out.WriteString(border) } } } func (t *Table) renderMarginRight(out *strings.Builder, hint renderHint) { if t.style.Options.DrawBorder { border := t.style.Box.Right if hint.isBorderTop { if t.title != "" { border = t.style.Box.RightSeparator } else { border = t.style.Box.TopRight } } else if hint.isBorderBottom { border = t.style.Box.BottomRight } else if hint.isSeparatorRow { if t.shouldMergeCellsVertically(t.numColumns-1, hint) { border = t.style.Box.Right } else { border = t.style.Box.RightSeparator } } colors := t.getBorderColors(hint) if colors.EscapeSeq() != "" { out.WriteString(colors.Sprint(border)) } else { out.WriteString(border) } } } func (t *Table) renderRow(out *strings.Builder, row rowStr, hint renderHint) { if len(row) > 0 { // fit every column into the allowedColumnLength/maxColumnLength limit // and in the process find the max. number of lines in any column in // this row colMaxLines := 0 rowWrapped := make(rowStr, len(row)) for colIdx, colStr := range row { widthEnforcer := t.columnConfigMap[colIdx].getWidthMaxEnforcer() rowWrapped[colIdx] = widthEnforcer(colStr, t.maxColumnLengths[colIdx]) colNumLines := strings.Count(rowWrapped[colIdx], "\n") + 1 if colNumLines > colMaxLines { colMaxLines = colNumLines } } // if there is just 1 line in all columns, add the row as such; else // split each column into individual lines and render them one-by-one if colMaxLines == 1 { hint.isLastLineOfRow = true t.renderLine(out, rowWrapped, hint) } else { // convert one row into N # of rows based on colMaxLines rowLines := make([]rowStr, len(row)) for colIdx, colStr := range rowWrapped { rowLines[colIdx] = t.getVAlign(colIdx, hint).ApplyStr(colStr, colMaxLines) } for colLineIdx := 0; colLineIdx < colMaxLines; colLineIdx++ { rowLine := make(rowStr, len(rowLines)) for colIdx, colLines := range rowLines { rowLine[colIdx] = colLines[colLineIdx] } hint.isLastLineOfRow = colLineIdx == colMaxLines-1 hint.rowLineNumber = colLineIdx + 1 t.renderLine(out, rowLine, hint) } } } } func (t *Table) renderRowSeparator(out *strings.Builder, hint renderHint) { if hint.isBorderTop || hint.isBorderBottom { if !t.style.Options.DrawBorder { return } } else if hint.isHeaderRow && !t.style.Options.SeparateHeader { return } else if hint.isFooterRow && !t.style.Options.SeparateFooter { return } hint.isSeparatorRow = true t.renderLine(out, t.rowSeparator, hint) } func (t *Table) renderRows(out *strings.Builder, rows []rowStr, hint renderHint) { for rowIdx, row := range rows { hint.isFirstRow = rowIdx == 0 hint.isLastRow = rowIdx == len(rows)-1 hint.rowNumber = rowIdx + 1 t.renderRow(out, row, hint) if (t.style.Options.SeparateRows && rowIdx < len(rows)-1) || // last row before footer (t.separators[rowIdx] && rowIdx != len(rows)-1) { // manually added separator not after last row hint.isFirstRow = false t.renderRowSeparator(out, hint) } } } func (t *Table) renderRowsBorderBottom(out *strings.Builder) { t.renderRowSeparator(out, renderHint{isBorderBottom: true, isFooterRow: true}) } func (t *Table) renderRowsBorderTop(out *strings.Builder) { t.renderRowSeparator(out, renderHint{isBorderTop: true, isHeaderRow: true}) } func (t *Table) renderRowsFooter(out *strings.Builder) { if len(t.rowsFooter) > 0 { t.renderRowSeparator(out, renderHint{ isFooterRow: true, isFirstRow: true, isSeparatorRow: true, }) t.renderRows(out, t.rowsFooter, renderHint{isFooterRow: true}) } } func (t *Table) renderRowsHeader(out *strings.Builder) { if len(t.rowsHeader) > 0 || t.autoIndex { if len(t.rowsHeader) > 0 { t.renderRows(out, t.rowsHeader, renderHint{isHeaderRow: true}) } else if t.autoIndex { t.renderRow(out, t.getAutoIndexColumnIDs(), renderHint{isAutoIndexRow: true, isHeaderRow: true}) } t.renderRowSeparator(out, renderHint{ isHeaderRow: true, isLastRow: true, isSeparatorRow: true, rowNumber: len(t.rowsHeader), }) } } func (t *Table) renderTitle(out *strings.Builder) { if t.title != "" { rowLength := t.maxRowLength if t.allowedRowLength != 0 && t.allowedRowLength < rowLength { rowLength = t.allowedRowLength } if t.style.Options.DrawBorder { lenBorder := rowLength - text.RuneCount(t.style.Box.TopLeft+t.style.Box.TopRight) out.WriteString(t.style.Box.TopLeft) out.WriteString(text.RepeatAndTrim(t.style.Box.MiddleHorizontal, lenBorder)) out.WriteString(t.style.Box.TopRight) } lenText := rowLength - text.RuneCount(t.style.Box.PaddingLeft+t.style.Box.PaddingRight) if t.style.Options.DrawBorder { lenText -= text.RuneCount(t.style.Box.Left + t.style.Box.Right) } titleText := text.WrapText(t.title, lenText) for _, titleLine := range strings.Split(titleText, "\n") { titleLine = strings.TrimSpace(titleLine) titleLine = t.style.Title.Format.Apply(titleLine) titleLine = t.style.Title.Align.Apply(titleLine, lenText) titleLine = t.style.Box.PaddingLeft + titleLine + t.style.Box.PaddingRight titleLine = t.style.Title.Colors.Sprint(titleLine) if out.Len() > 0 { out.WriteRune('\n') } if t.style.Options.DrawBorder { out.WriteString(t.style.Box.Left) } out.WriteString(titleLine) if t.style.Options.DrawBorder { out.WriteString(t.style.Box.Right) } } } } go-pretty-6.2.4/table/render_csv.go000066400000000000000000000040631407250454200172330ustar00rootroot00000000000000package table import ( "fmt" "strings" "unicode/utf8" ) // RenderCSV renders the Table in CSV format. Example: // #,First Name,Last Name,Salary, // 1,Arya,Stark,3000, // 20,Jon,Snow,2000,"You know nothing\, Jon Snow!" // 300,Tyrion,Lannister,5000, // ,,Total,10000, func (t *Table) RenderCSV() string { t.initForRender() var out strings.Builder if t.numColumns > 0 { if t.title != "" { out.WriteString(t.title) } if t.autoIndex && len(t.rowsHeader) == 0 { t.csvRenderRow(&out, t.getAutoIndexColumnIDs(), renderHint{isAutoIndexRow: true, isHeaderRow: true}) } t.csvRenderRows(&out, t.rowsHeader, renderHint{isHeaderRow: true}) t.csvRenderRows(&out, t.rows, renderHint{}) t.csvRenderRows(&out, t.rowsFooter, renderHint{isFooterRow: true}) if t.caption != "" { out.WriteRune('\n') out.WriteString(t.caption) } } return t.render(&out) } func (t *Table) csvFixCommas(str string) string { return strings.Replace(str, ",", "\\,", -1) } func (t *Table) csvFixDoubleQuotes(str string) string { return strings.Replace(str, "\"", "\\\"", -1) } func (t *Table) csvRenderRow(out *strings.Builder, row rowStr, hint renderHint) { // when working on line number 2 or more, insert a newline first if out.Len() > 0 { out.WriteRune('\n') } // generate the columns to render in CSV format and append to "out" for colIdx, colStr := range row { // auto-index column if colIdx == 0 && t.autoIndex { if hint.isRegularRow() { out.WriteString(fmt.Sprint(hint.rowNumber)) } out.WriteRune(',') } if colIdx > 0 { out.WriteRune(',') } if strings.ContainsAny(colStr, "\",\n") { out.WriteRune('"') out.WriteString(t.csvFixCommas(t.csvFixDoubleQuotes(colStr))) out.WriteRune('"') } else if utf8.RuneCountInString(colStr) > 0 { out.WriteString(colStr) } } for colIdx := len(row); colIdx < t.numColumns; colIdx++ { out.WriteRune(',') } } func (t *Table) csvRenderRows(out *strings.Builder, rows []rowStr, hint renderHint) { for rowIdx, row := range rows { hint.rowNumber = rowIdx + 1 t.csvRenderRow(out, row, hint) } } go-pretty-6.2.4/table/render_csv_test.go000066400000000000000000000072031407250454200202710ustar00rootroot00000000000000package table import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestTable_RenderCSV(t *testing.T) { tw := NewWriter() tw.AppendHeader(testHeader) tw.AppendRows(testRows) tw.AppendRow(testRowMultiLine) tw.AppendRow(testRowTabs) tw.AppendFooter(testFooter) tw.SetCaption(testCaption) tw.SetTitle(testTitle1) expectedOut := `Game of Thrones #,First Name,Last Name,Salary, 1,Arya,Stark,3000, 20,Jon,Snow,2000,"You know nothing\, Jon Snow!" 300,Tyrion,Lannister,5000, 0,Winter,Is,0,"Coming. The North Remembers! This is known." 0,Valar,Morghulis,0,Faceless Men ,,Total,10000, A Song of Ice and Fire` assert.Equal(t, expectedOut, tw.RenderCSV()) } func TestTable_RenderCSV_AutoIndex(t *testing.T) { tw := NewWriter() for rowIdx := 0; rowIdx < 10; rowIdx++ { row := make(Row, 10) for colIdx := 0; colIdx < 10; colIdx++ { row[colIdx] = fmt.Sprintf("%s%d", AutoIndexColumnID(colIdx), rowIdx+1) } tw.AppendRow(row) } for rowIdx := 0; rowIdx < 1; rowIdx++ { row := make(Row, 10) for colIdx := 0; colIdx < 10; colIdx++ { row[colIdx] = AutoIndexColumnID(colIdx) + "F" } tw.AppendFooter(row) } tw.SetAutoIndex(true) tw.SetStyle(StyleLight) expectedOut := `,A,B,C,D,E,F,G,H,I,J 1,A1,B1,C1,D1,E1,F1,G1,H1,I1,J1 2,A2,B2,C2,D2,E2,F2,G2,H2,I2,J2 3,A3,B3,C3,D3,E3,F3,G3,H3,I3,J3 4,A4,B4,C4,D4,E4,F4,G4,H4,I4,J4 5,A5,B5,C5,D5,E5,F5,G5,H5,I5,J5 6,A6,B6,C6,D6,E6,F6,G6,H6,I6,J6 7,A7,B7,C7,D7,E7,F7,G7,H7,I7,J7 8,A8,B8,C8,D8,E8,F8,G8,H8,I8,J8 9,A9,B9,C9,D9,E9,F9,G9,H9,I9,J9 10,A10,B10,C10,D10,E10,F10,G10,H10,I10,J10 ,AF,BF,CF,DF,EF,FF,GF,HF,IF,JF` assert.Equal(t, expectedOut, tw.RenderCSV()) } func TestTable_RenderCSV_Empty(t *testing.T) { tw := NewWriter() assert.Empty(t, tw.RenderCSV()) } func TestTable_RenderCSV_HiddenColumns(t *testing.T) { tw := NewWriter() tw.AppendHeader(testHeader) tw.AppendRows(testRows) tw.AppendFooter(testFooter) // ensure sorting is done before hiding the columns tw.SortBy([]SortBy{ {Name: "Salary", Mode: DscNumeric}, }) t.Run("every column hidden", func(t *testing.T) { tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{0, 1, 2, 3, 4})) expectedOut := `` assert.Equal(t, expectedOut, tw.RenderCSV()) }) t.Run("first column hidden", func(t *testing.T) { tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{0})) expectedOut := `First Name,Last Name,Salary, >>Tyrion,Lannister<<,5013, >>Arya,Stark<<,3013, >>Jon,Snow<<,2013,"~You know nothing\, Jon Snow!~" ,Total,10000,` assert.Equal(t, expectedOut, tw.RenderCSV()) }) t.Run("column hidden in the middle", func(t *testing.T) { tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{1})) expectedOut := `#,Last Name,Salary, 307,Lannister<<,5013, 8,Stark<<,3013, 27,Snow<<,2013,"~You know nothing\, Jon Snow!~" ,Total,10000,` assert.Equal(t, expectedOut, tw.RenderCSV()) }) t.Run("last column hidden", func(t *testing.T) { tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{4})) expectedOut := `#,First Name,Last Name,Salary 307,>>Tyrion,Lannister<<,5013 8,>>Arya,Stark<<,3013 27,>>Jon,Snow<<,2013 ,,Total,10000` assert.Equal(t, expectedOut, tw.RenderCSV()) }) } func TestTable_RenderCSV_Sorted(t *testing.T) { tw := NewWriter() tw.AppendHeader(testHeader) tw.AppendRows(testRows) tw.AppendRow(Row{11, "Sansa", "Stark", 6000}) tw.AppendFooter(testFooter) tw.SortBy([]SortBy{{Name: "Last Name", Mode: Asc}, {Name: "First Name", Mode: Asc}}) expectedOut := `#,First Name,Last Name,Salary, 300,Tyrion,Lannister,5000, 20,Jon,Snow,2000,"You know nothing\, Jon Snow!" 1,Arya,Stark,3000, 11,Sansa,Stark,6000, ,,Total,10000,` assert.Equal(t, expectedOut, tw.RenderCSV()) } go-pretty-6.2.4/table/render_html.go000066400000000000000000000133501407250454200174030ustar00rootroot00000000000000package table import ( "fmt" "html" "strings" ) const ( // DefaultHTMLCSSClass stores the css-class to use when none-provided via // SetHTMLCSSClass(cssClass string). DefaultHTMLCSSClass = "go-pretty-table" ) // RenderHTML renders the Table in HTML format. Example: // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // //
          #First NameLast NameSalary 
          1AryaStark3000 
          20JonSnow2000You know nothing, Jon Snow!
          300TyrionLannister5000 
            Total10000 
          func (t *Table) RenderHTML() string { t.initForRender() var out strings.Builder if t.numColumns > 0 { out.WriteString("\n") t.htmlRenderTitle(&out) t.htmlRenderRowsHeader(&out) t.htmlRenderRows(&out, t.rows, renderHint{}) t.htmlRenderRowsFooter(&out) t.htmlRenderCaption(&out) out.WriteString("
          ") } return t.render(&out) } func (t *Table) htmlRenderCaption(out *strings.Builder) { if t.caption != "" { out.WriteString(" ") out.WriteString(t.caption) out.WriteString("\n") } } func (t *Table) htmlRenderColumnAttributes(out *strings.Builder, row rowStr, colIdx int, hint renderHint) { // determine the HTML "align"/"valign" property values align := t.getAlign(colIdx, hint).HTMLProperty() vAlign := t.getVAlign(colIdx, hint).HTMLProperty() // determine the HTML "class" property values for the colors class := t.getColumnColors(colIdx, hint).HTMLProperty() if align != "" { out.WriteRune(' ') out.WriteString(align) } if class != "" { out.WriteRune(' ') out.WriteString(class) } if vAlign != "" { out.WriteRune(' ') out.WriteString(vAlign) } } func (t *Table) htmlRenderColumnAutoIndex(out *strings.Builder, hint renderHint) { if hint.isHeaderRow { out.WriteString(" ") out.WriteString(t.style.HTML.EmptyColumn) out.WriteString("\n") } else if hint.isFooterRow { out.WriteString(" ") out.WriteString(t.style.HTML.EmptyColumn) out.WriteString("\n") } else { out.WriteString(" ") out.WriteString(fmt.Sprint(hint.rowNumber)) out.WriteString("\n") } } func (t *Table) htmlRenderRow(out *strings.Builder, row rowStr, hint renderHint) { out.WriteString(" \n") for colIdx := 0; colIdx < t.numColumns; colIdx++ { // auto-index column if colIdx == 0 && t.autoIndex { t.htmlRenderColumnAutoIndex(out, hint) } // get the column contents var colStr string if colIdx < len(row) { colStr = row[colIdx] } // header uses "th" instead of "td" colTagName := "td" if hint.isHeaderRow { colTagName = "th" } // write the row out.WriteString(" <") out.WriteString(colTagName) t.htmlRenderColumnAttributes(out, row, colIdx, hint) out.WriteString(">") if len(colStr) == 0 { out.WriteString(t.style.HTML.EmptyColumn) } else { if t.style.HTML.EscapeText { colStr = html.EscapeString(colStr) } if t.style.HTML.Newline != "\n" { colStr = strings.Replace(colStr, "\n", t.style.HTML.Newline, -1) } out.WriteString(colStr) } out.WriteString("\n") } out.WriteString(" \n") } func (t *Table) htmlRenderRows(out *strings.Builder, rows []rowStr, hint renderHint) { if len(rows) > 0 { // determine that tag to use based on the type of the row rowsTag := "tbody" if hint.isHeaderRow { rowsTag = "thead" } else if hint.isFooterRow { rowsTag = "tfoot" } var renderedTagOpen, shouldRenderTagClose bool for idx, row := range rows { hint.rowNumber = idx + 1 if len(row) > 0 { if !renderedTagOpen { out.WriteString(" <") out.WriteString(rowsTag) out.WriteString(">\n") renderedTagOpen = true } t.htmlRenderRow(out, row, hint) shouldRenderTagClose = true } } if shouldRenderTagClose { out.WriteString(" \n") } } } func (t *Table) htmlRenderRowsFooter(out *strings.Builder) { if len(t.rowsFooter) > 0 { t.htmlRenderRows(out, t.rowsFooter, renderHint{isFooterRow: true}) } } func (t *Table) htmlRenderRowsHeader(out *strings.Builder) { if len(t.rowsHeader) > 0 { t.htmlRenderRows(out, t.rowsHeader, renderHint{isHeaderRow: true}) } else if t.autoIndex { hint := renderHint{isAutoIndexRow: true, isHeaderRow: true} t.htmlRenderRows(out, []rowStr{t.getAutoIndexColumnIDs()}, hint) } } func (t *Table) htmlRenderTitle(out *strings.Builder) { if t.title != "" { align := t.style.Title.Align.HTMLProperty() colors := t.style.Title.Colors.HTMLProperty() title := t.style.Title.Format.Apply(t.title) out.WriteString(" ') out.WriteString(title) out.WriteString("\n") } } go-pretty-6.2.4/table/render_html_test.go000066400000000000000000000305751407250454200204520ustar00rootroot00000000000000package table import ( "fmt" "os" "testing" "github.com/jedib0t/go-pretty/v6/text" "github.com/stretchr/testify/assert" ) func TestTable_RenderHTML(t *testing.T) { tw := NewWriter() tw.AppendHeader(testHeader) tw.AppendRows(testRows) tw.AppendRow(testRowMultiLine) tw.AppendFooter(testFooter) tw.SetColumnConfigs([]ColumnConfig{ {Name: "Salary", VAlign: text.VAlignBottom}, {Number: 5, VAlign: text.VAlignBottom}, }) tw.SetTitle(testTitle1) tw.SetCaption(testCaption) tw.Style().Title = TitleOptions{ Align: text.AlignLeft, Colors: text.Colors{text.BgBlack, text.Bold, text.FgHiBlue}, Format: text.FormatTitle, } expectedOut := `
          Game Of Thrones
          # First Name Last Name Salary  
          1 Arya Stark 3000  
          20 Jon Snow 2000 You know nothing, Jon Snow!
          300 Tyrion Lannister 5000  
          0 Winter Is 0 Coming.
          The North Remembers!
          This is known.
              Total 10000  
          A Song of Ice and Fire
          ` assert.Equal(t, expectedOut, tw.RenderHTML()) } func TestTable_RenderHTML_AutoIndex(t *testing.T) { tw := NewWriter() for rowIdx := 0; rowIdx < 3; rowIdx++ { row := make(Row, 3) for colIdx := 0; colIdx < 3; colIdx++ { row[colIdx] = fmt.Sprintf("%s%d", AutoIndexColumnID(colIdx), rowIdx+1) } tw.AppendRow(row) } for rowIdx := 0; rowIdx < 1; rowIdx++ { row := make(Row, 3) for colIdx := 0; colIdx < 3; colIdx++ { row[colIdx] = AutoIndexColumnID(colIdx) + "F" } tw.AppendFooter(row) } tw.SetOutputMirror(os.Stdout) tw.SetAutoIndex(true) tw.SetStyle(StyleLight) expectedOut := `
            A B C
          1 A1 B1 C1
          2 A2 B2 C2
          3 A3 B3 C3
            AF BF CF
          ` assert.Equal(t, expectedOut, tw.RenderHTML()) } func TestTable_RenderHTML_Colored(t *testing.T) { tw := NewWriter() tw.AppendHeader(testHeader) tw.AppendRows(testRows) tw.AppendRow(testRowMultiLine) tw.AppendFooter(testFooter) tw.SetCaption(testCaption) tw.SetTitle(testTitle1) tw.Style().HTML.CSSClass = "go-pretty-table-colored" colorsBlackOnWhite := text.Colors{text.BgWhite, text.FgBlack} tw.SetColumnConfigs([]ColumnConfig{ { Name: "#", Colors: text.Colors{text.Bold}, ColorsHeader: colorsBlackOnWhite, }, { Name: "First Name", Colors: text.Colors{text.FgCyan}, ColorsHeader: colorsBlackOnWhite, }, { Name: "Last Name", Colors: text.Colors{text.FgMagenta}, ColorsHeader: colorsBlackOnWhite, ColorsFooter: colorsBlackOnWhite, }, { Name: "Salary", Colors: text.Colors{text.FgYellow}, ColorsHeader: colorsBlackOnWhite, ColorsFooter: colorsBlackOnWhite, VAlign: text.VAlignBottom, }, { Number: 5, Colors: text.Colors{text.FgBlack}, ColorsHeader: colorsBlackOnWhite, VAlign: text.VAlignBottom, }, }) expectedOut := `
          Game of Thrones
          # First Name Last Name Salary  
          1 Arya Stark 3000  
          20 Jon Snow 2000 You know nothing, Jon Snow!
          300 Tyrion Lannister 5000  
          0 Winter Is 0 Coming.
          The North Remembers!
          This is known.
              Total 10000  
          A Song of Ice and Fire
          ` assert.Equal(t, expectedOut, tw.RenderHTML()) } func TestTable_RenderHTML_CustomStyle(t *testing.T) { tw := NewWriter() tw.AppendHeader(testHeader) tw.AppendRow(Row{1, "Arya", "Stark", 3000, "Not today."}) tw.AppendRow(Row{1, "Jon", "Snow", 2000, "You know\nnothing,\nJon Snow!"}) tw.AppendRow(Row{300, "Tyrion", "Lannister", 5000}) tw.AppendFooter(testFooter) tw.SetAutoIndex(true) tw.Style().HTML = HTMLOptions{ CSSClass: "game-of-thrones", EmptyColumn: " ", EscapeText: false, Newline: "", } tw.SetOutputMirror(os.Stdout) expectedOut := `
            # First Name Last Name Salary  
          1 1 Arya Stark 3000 Not today.
          2 1 Jon Snow 2000 You knownothing,Jon Snow!
          3 300 Tyrion Lannister 5000  
                Total 10000  
          ` assert.Equal(t, expectedOut, tw.RenderHTML()) } func TestTable_RenderHTML_Empty(t *testing.T) { tw := NewWriter() assert.Empty(t, tw.RenderHTML()) } func TestTable_RenderHTML_HiddenColumns(t *testing.T) { tw := NewWriter() tw.AppendHeader(testHeader) tw.AppendRows(testRows) tw.AppendFooter(testFooter) // ensure sorting is done before hiding the columns tw.SortBy([]SortBy{ {Name: "Salary", Mode: DscNumeric}, }) t.Run("every column hidden", func(t *testing.T) { tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{0, 1, 2, 3, 4})) expectedOut := `` assert.Equal(t, expectedOut, tw.RenderHTML()) }) t.Run("first column hidden", func(t *testing.T) { tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{0})) expectedOut := `
          First Name Last Name Salary  
          >>Tyrion Lannister<< 5013  
          >>Arya Stark<< 3013  
          >>Jon Snow<< 2013 ~You know nothing, Jon Snow!~
            Total 10000  
          ` assert.Equal(t, expectedOut, tw.RenderHTML()) }) t.Run("column hidden in the middle", func(t *testing.T) { tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{1})) expectedOut := `
          # Last Name Salary  
          307 Lannister<< 5013  
          8 Stark<< 3013  
          27 Snow<< 2013 ~You know nothing, Jon Snow!~
            Total 10000  
          ` assert.Equal(t, expectedOut, tw.RenderHTML()) }) t.Run("last column hidden", func(t *testing.T) { tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{4})) expectedOut := `
          # First Name Last Name Salary
          307 >>Tyrion Lannister<< 5013
          8 >>Arya Stark<< 3013
          27 >>Jon Snow<< 2013
              Total 10000
          ` assert.Equal(t, expectedOut, tw.RenderHTML()) }) } func TestTable_RenderHTML_Sorted(t *testing.T) { tw := NewWriter() tw.AppendHeader(testHeader) tw.AppendRows(testRows) tw.AppendRow(Row{11, "Sansa", "Stark", 6000}) tw.AppendFooter(testFooter) tw.SortBy([]SortBy{{Name: "Last Name", Mode: Asc}, {Name: "First Name", Mode: Asc}}) expectedOut := `
          # First Name Last Name Salary  
          300 Tyrion Lannister 5000  
          20 Jon Snow 2000 You know nothing, Jon Snow!
          1 Arya Stark 3000  
          11 Sansa Stark 6000  
              Total 10000  
          ` assert.Equal(t, expectedOut, tw.RenderHTML()) } go-pretty-6.2.4/table/render_markdown.go000066400000000000000000000055041407250454200202630ustar00rootroot00000000000000package table import ( "fmt" "strings" ) // RenderMarkdown renders the Table in Markdown format. Example: // | # | First Name | Last Name | Salary | | // | ---:| --- | --- | ---:| --- | // | 1 | Arya | Stark | 3000 | | // | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | // | 300 | Tyrion | Lannister | 5000 | | // | | | Total | 10000 | | func (t *Table) RenderMarkdown() string { t.initForRender() var out strings.Builder if t.numColumns > 0 { t.markdownRenderTitle(&out) t.markdownRenderRowsHeader(&out) t.markdownRenderRows(&out, t.rows, renderHint{}) t.markdownRenderRowsFooter(&out) t.markdownRenderCaption(&out) } return t.render(&out) } func (t *Table) markdownRenderCaption(out *strings.Builder) { if t.caption != "" { out.WriteRune('\n') out.WriteRune('_') out.WriteString(t.caption) out.WriteRune('_') } } func (t *Table) markdownRenderRow(out *strings.Builder, row rowStr, hint renderHint) { // when working on line number 2 or more, insert a newline first if out.Len() > 0 { out.WriteRune('\n') } // render each column up to the max. columns seen in all the rows out.WriteRune('|') for colIdx := 0; colIdx < t.numColumns; colIdx++ { // auto-index column if colIdx == 0 && t.autoIndex { out.WriteRune(' ') if hint.isSeparatorRow { out.WriteString("---:") } else if hint.isRegularRow() { out.WriteString(fmt.Sprintf("%d ", hint.rowNumber)) } out.WriteRune('|') } if hint.isSeparatorRow { out.WriteString(t.getAlign(colIdx, hint).MarkdownProperty()) } else { var colStr string if colIdx < len(row) { colStr = row[colIdx] } out.WriteRune(' ') if strings.Contains(colStr, "|") { colStr = strings.Replace(colStr, "|", "\\|", -1) } if strings.Contains(colStr, "\n") { colStr = strings.Replace(colStr, "\n", "
          ", -1) } out.WriteString(colStr) out.WriteRune(' ') } out.WriteRune('|') } } func (t *Table) markdownRenderRows(out *strings.Builder, rows []rowStr, hint renderHint) { if len(rows) > 0 { for idx, row := range rows { hint.rowNumber = idx + 1 t.markdownRenderRow(out, row, hint) if idx == len(rows)-1 && hint.isHeaderRow { t.markdownRenderRow(out, t.rowSeparator, renderHint{isSeparatorRow: true}) } } } } func (t *Table) markdownRenderRowsFooter(out *strings.Builder) { t.markdownRenderRows(out, t.rowsFooter, renderHint{isFooterRow: true}) } func (t *Table) markdownRenderRowsHeader(out *strings.Builder) { if len(t.rowsHeader) > 0 { t.markdownRenderRows(out, t.rowsHeader, renderHint{isHeaderRow: true}) } else if t.autoIndex { t.markdownRenderRows(out, []rowStr{t.getAutoIndexColumnIDs()}, renderHint{isAutoIndexRow: true, isHeaderRow: true}) } } func (t *Table) markdownRenderTitle(out *strings.Builder) { if t.title != "" { out.WriteString("# ") out.WriteString(t.title) } } go-pretty-6.2.4/table/render_markdown_test.go000066400000000000000000000107051407250454200213210ustar00rootroot00000000000000package table import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestTable_RenderMarkdown(t *testing.T) { tw := NewWriter() tw.AppendHeader(testHeader) tw.AppendRows(testRows) tw.AppendRow(testRowNewLines) tw.AppendRow(testRowPipes) tw.AppendFooter(testFooter) tw.SetCaption(testCaption) tw.SetTitle(testTitle1) expectedOut := `# Game of Thrones | # | First Name | Last Name | Salary | | | ---:| --- | --- | ---:| --- | | 1 | Arya | Stark | 3000 | | | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | | 300 | Tyrion | Lannister | 5000 | | | 0 | Valar | Morghulis | 0 | Faceless
          Men | | 0 | Valar | Morghulis | 0 | Faceless\|Men | | | | Total | 10000 | | _A Song of Ice and Fire_` assert.Equal(t, expectedOut, tw.RenderMarkdown()) } func TestTable_RenderMarkdown_AutoIndex(t *testing.T) { tw := NewWriter() for rowIdx := 0; rowIdx < 10; rowIdx++ { row := make(Row, 10) for colIdx := 0; colIdx < 10; colIdx++ { row[colIdx] = fmt.Sprintf("%s%d", AutoIndexColumnID(colIdx), rowIdx+1) } tw.AppendRow(row) } for rowIdx := 0; rowIdx < 1; rowIdx++ { row := make(Row, 10) for colIdx := 0; colIdx < 10; colIdx++ { row[colIdx] = AutoIndexColumnID(colIdx) + "F" } tw.AppendFooter(row) } tw.SetAutoIndex(true) tw.SetStyle(StyleLight) expectedOut := `| | A | B | C | D | E | F | G | H | I | J | | ---:| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | 1 | A1 | B1 | C1 | D1 | E1 | F1 | G1 | H1 | I1 | J1 | | 2 | A2 | B2 | C2 | D2 | E2 | F2 | G2 | H2 | I2 | J2 | | 3 | A3 | B3 | C3 | D3 | E3 | F3 | G3 | H3 | I3 | J3 | | 4 | A4 | B4 | C4 | D4 | E4 | F4 | G4 | H4 | I4 | J4 | | 5 | A5 | B5 | C5 | D5 | E5 | F5 | G5 | H5 | I5 | J5 | | 6 | A6 | B6 | C6 | D6 | E6 | F6 | G6 | H6 | I6 | J6 | | 7 | A7 | B7 | C7 | D7 | E7 | F7 | G7 | H7 | I7 | J7 | | 8 | A8 | B8 | C8 | D8 | E8 | F8 | G8 | H8 | I8 | J8 | | 9 | A9 | B9 | C9 | D9 | E9 | F9 | G9 | H9 | I9 | J9 | | 10 | A10 | B10 | C10 | D10 | E10 | F10 | G10 | H10 | I10 | J10 | | | AF | BF | CF | DF | EF | FF | GF | HF | IF | JF |` assert.Equal(t, expectedOut, tw.RenderMarkdown()) } func TestTable_RenderMarkdown_Empty(t *testing.T) { tw := NewWriter() assert.Empty(t, tw.RenderMarkdown()) } func TestTable_RenderMarkdown_HiddenColumns(t *testing.T) { tw := NewWriter() tw.AppendHeader(testHeader) tw.AppendRows(testRows) tw.AppendFooter(testFooter) // ensure sorting is done before hiding the columns tw.SortBy([]SortBy{ {Name: "Salary", Mode: DscNumeric}, }) t.Run("every column hidden", func(t *testing.T) { tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{0, 1, 2, 3, 4})) expectedOut := `` assert.Equal(t, expectedOut, tw.RenderMarkdown()) }) t.Run("first column hidden", func(t *testing.T) { tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{0})) expectedOut := `| First Name | Last Name | Salary | | | --- | --- | ---:| --- | | >>Tyrion | Lannister<< | 5013 | | | >>Arya | Stark<< | 3013 | | | >>Jon | Snow<< | 2013 | ~You know nothing, Jon Snow!~ | | | Total | 10000 | |` assert.Equal(t, expectedOut, tw.RenderMarkdown()) }) t.Run("column hidden in the middle", func(t *testing.T) { tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{1})) expectedOut := `| # | Last Name | Salary | | | ---:| --- | ---:| --- | | 307 | Lannister<< | 5013 | | | 8 | Stark<< | 3013 | | | 27 | Snow<< | 2013 | ~You know nothing, Jon Snow!~ | | | Total | 10000 | |` assert.Equal(t, expectedOut, tw.RenderMarkdown()) }) t.Run("last column hidden", func(t *testing.T) { tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{4})) expectedOut := `| # | First Name | Last Name | Salary | | ---:| --- | --- | ---:| | 307 | >>Tyrion | Lannister<< | 5013 | | 8 | >>Arya | Stark<< | 3013 | | 27 | >>Jon | Snow<< | 2013 | | | | Total | 10000 |` assert.Equal(t, expectedOut, tw.RenderMarkdown()) }) } func TestTable_RendeMarkdown_Sorted(t *testing.T) { tw := NewWriter() tw.AppendHeader(testHeader) tw.AppendRows(testRows) tw.AppendRow(Row{11, "Sansa", "Stark", 6000}) tw.AppendFooter(testFooter) tw.SortBy([]SortBy{{Name: "Last Name", Mode: Asc}, {Name: "First Name", Mode: Asc}}) expectedOut := `| # | First Name | Last Name | Salary | | | ---:| --- | --- | ---:| --- | | 300 | Tyrion | Lannister | 5000 | | | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | | 1 | Arya | Stark | 3000 | | | 11 | Sansa | Stark | 6000 | | | | | Total | 10000 | |` assert.Equal(t, expectedOut, tw.RenderMarkdown()) } go-pretty-6.2.4/table/render_test.go000066400000000000000000003016111407250454200174160ustar00rootroot00000000000000package table import ( "fmt" "sort" "strings" "testing" "github.com/jedib0t/go-pretty/v6/text" "github.com/stretchr/testify/assert" ) func generateColumnConfigsWithHiddenColumns(colsToHide []int) []ColumnConfig { cc := []ColumnConfig{ { Name: "#", Transformer: func(val interface{}) string { return fmt.Sprint(val.(int) + 7) }, }, { Name: "First Name", Transformer: func(val interface{}) string { return fmt.Sprintf(">>%s", val) }, }, { Name: "Last Name", Transformer: func(val interface{}) string { return fmt.Sprintf("%s<<", val) }, }, { Name: "Salary", Transformer: func(val interface{}) string { return fmt.Sprint(val.(int) + 13) }, }, { Number: 5, Transformer: func(val interface{}) string { return fmt.Sprintf("~%s~", val) }, }, } for _, colToHide := range colsToHide { cc[colToHide].Hidden = true } return cc } func TestTable_Render(t *testing.T) { tw := NewWriter() tw.AppendHeader(testHeader) tw.AppendRows(testRows) tw.AppendRow(testRowMultiLine) tw.AppendFooter(testFooter) tw.SetCaption(testCaption) tw.SetStyle(styleTest) tw.SetTitle(testTitle2) expectedOut := `(---------------------------------------------------------------------) [] [] {-----^------------^-----------^--------^-----------------------------} [< #>||||< >] {-----+------------+-----------+--------+-----------------------------} [< 1>|||< 3000>|< >] [< 20>|||< 2000>|] [<300>|||< 5000>|< >] [< 0>|||< 0>|] [< >|< >|< >|< >|] [< >|< >|< >|< >|] {-----+------------+-----------+--------+-----------------------------} [< >|< >||< 10000>|< >] \-----v------------v-----------v--------v-----------------------------/ A Song of Ice and Fire` assert.Equal(t, expectedOut, tw.Render()) } func TestTable_Render_AutoIndex(t *testing.T) { tw := NewWriter() for rowIdx := 0; rowIdx < 10; rowIdx++ { row := make(Row, 10) for colIdx := 0; colIdx < 10; colIdx++ { row[colIdx] = fmt.Sprintf("%s%d", AutoIndexColumnID(colIdx), rowIdx+1) } tw.AppendRow(row) } tw.SetAutoIndex(true) tw.SetStyle(StyleLight) expectedOut := `┌────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐ │ │ A │ B │ C │ D │ E │ F │ G │ H │ I │ J │ ├────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤ │ 1 │ A1 │ B1 │ C1 │ D1 │ E1 │ F1 │ G1 │ H1 │ I1 │ J1 │ │ 2 │ A2 │ B2 │ C2 │ D2 │ E2 │ F2 │ G2 │ H2 │ I2 │ J2 │ │ 3 │ A3 │ B3 │ C3 │ D3 │ E3 │ F3 │ G3 │ H3 │ I3 │ J3 │ │ 4 │ A4 │ B4 │ C4 │ D4 │ E4 │ F4 │ G4 │ H4 │ I4 │ J4 │ │ 5 │ A5 │ B5 │ C5 │ D5 │ E5 │ F5 │ G5 │ H5 │ I5 │ J5 │ │ 6 │ A6 │ B6 │ C6 │ D6 │ E6 │ F6 │ G6 │ H6 │ I6 │ J6 │ │ 7 │ A7 │ B7 │ C7 │ D7 │ E7 │ F7 │ G7 │ H7 │ I7 │ J7 │ │ 8 │ A8 │ B8 │ C8 │ D8 │ E8 │ F8 │ G8 │ H8 │ I8 │ J8 │ │ 9 │ A9 │ B9 │ C9 │ D9 │ E9 │ F9 │ G9 │ H9 │ I9 │ J9 │ │ 10 │ A10 │ B10 │ C10 │ D10 │ E10 │ F10 │ G10 │ H10 │ I10 │ J10 │ └────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘` assert.Equal(t, expectedOut, tw.Render()) } func TestTable_Render_AutoMerge(t *testing.T) { rowConfigAutoMerge := RowConfig{AutoMerge: true} tw := NewWriter() tw.AppendHeader(Row{"Node IP", "Pods", "Namespace", "Container", "RCE", "RCE"}, rowConfigAutoMerge) tw.AppendHeader(Row{"", "", "", "", "EXE", "RUN"}) tw.AppendRow(Row{"1.1.1.1", "Pod 1A", "NS 1A", "C 1", "Y", "Y"}, rowConfigAutoMerge) tw.AppendRow(Row{"1.1.1.1", "Pod 1A", "NS 1A", "C 2", "Y", "N"}, rowConfigAutoMerge) tw.AppendRow(Row{"1.1.1.1", "Pod 1A", "NS 1B", "C 3", "N", "N"}, rowConfigAutoMerge) tw.AppendRow(Row{"1.1.1.1", "Pod 1B", "NS 2", "C 4", "N", "N"}, rowConfigAutoMerge) tw.AppendRow(Row{"1.1.1.1", "Pod 1B", "NS 2", "C 5", "Y", "N"}, rowConfigAutoMerge) tw.AppendRow(Row{"2.2.2.2", "Pod 2", "NS 3", "C 6", "Y", "Y"}, rowConfigAutoMerge) tw.AppendRow(Row{"2.2.2.2", "Pod 2", "NS 3", "C 7", "Y", "Y"}, rowConfigAutoMerge) tw.AppendFooter(Row{"", "", "", 7, 5, 3}) tw.SetAutoIndex(true) tw.SetColumnConfigs([]ColumnConfig{ {Number: 1, AutoMerge: true}, {Number: 2, AutoMerge: true}, {Number: 3, AutoMerge: true}, {Number: 4, AutoMerge: true}, {Number: 5, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter}, {Number: 6, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter}, }) tw.SetStyle(StyleLight) tw.Style().Options.SeparateRows = true expectedOut := `┌───┬─────────┬────────┬───────────┬───────────┬───────────┐ │ │ NODE IP │ PODS │ NAMESPACE │ CONTAINER │ RCE │ │ │ │ │ │ ├─────┬─────┤ │ │ │ │ │ │ EXE │ RUN │ ├───┼─────────┼────────┼───────────┼───────────┼─────┴─────┤ │ 1 │ 1.1.1.1 │ Pod 1A │ NS 1A │ C 1 │ Y │ ├───┤ │ │ ├───────────┼─────┬─────┤ │ 2 │ │ │ │ C 2 │ Y │ N │ ├───┤ │ ├───────────┼───────────┼─────┴─────┤ │ 3 │ │ │ NS 1B │ C 3 │ N │ ├───┤ ├────────┼───────────┼───────────┼───────────┤ │ 4 │ │ Pod 1B │ NS 2 │ C 4 │ N │ ├───┤ │ │ ├───────────┼─────┬─────┤ │ 5 │ │ │ │ C 5 │ Y │ N │ ├───┼─────────┼────────┼───────────┼───────────┼─────┴─────┤ │ 6 │ 2.2.2.2 │ Pod 2 │ NS 3 │ C 6 │ Y │ ├───┤ │ │ ├───────────┼───────────┤ │ 7 │ │ │ │ C 7 │ Y │ ├───┼─────────┼────────┼───────────┼───────────┼─────┬─────┤ │ │ │ │ │ 7 │ 5 │ 3 │ └───┴─────────┴────────┴───────────┴───────────┴─────┴─────┘` assert.Equal(t, expectedOut, tw.Render()) } func TestTable_Render_AutoMerge_Complex(t *testing.T) { tw := NewWriter() tw.AppendHeader(Row{"Node IP", "Pods", "Namespace", "Container", "RCE", "RCE", "ID"}, RowConfig{AutoMerge: true}) tw.AppendHeader(Row{"", "", "", "", "EXE", "RUN", ""}) tw.AppendRow(Row{"1.1.1.1", "Pod 1A", "NS 1A", "C 1", "Y", "Y", 123}, RowConfig{AutoMerge: true}) tw.AppendRow(Row{"1.1.1.1", "Pod 1A", "NS 1A", "C 2", "Y", "N", 234}) tw.AppendRow(Row{"1.1.1.1", "Pod 1A", "NS 1B", "C 3", "N", "N", 345}) tw.AppendRow(Row{"1.1.1.1", "Pod 1B", "NS 2", "C 4", "N", "N", 456}, RowConfig{AutoMerge: true}) tw.AppendRow(Row{"1.1.1.1", "Pod 1B", "NS 2", "C 5", "Y", "N", 567}) tw.AppendRow(Row{"2.2.2.2", "Pod 2", "NS 3", "C 6", "Y", "Y", 678}, RowConfig{AutoMerge: true}) tw.AppendRow(Row{"2.2.2.2", "Pod 2", "NS 3", "C 7", "Y", "Y", 789}, RowConfig{AutoMerge: true}) tw.AppendFooter(Row{"", "", "", 7, 5, 5}, RowConfig{AutoMerge: true}) tw.AppendFooter(Row{"", "", "", 7, 5, 3}, RowConfig{AutoMerge: true}) tw.AppendFooter(Row{"", "", "", 7, 5, 5}, RowConfig{AutoMerge: true}) tw.AppendFooter(Row{"", "", "", 7, 5, 3}, RowConfig{AutoMerge: true}) tw.AppendFooter(Row{"", "", "", 7, 5, 5}, RowConfig{AutoMerge: true}) tw.SetAutoIndex(true) tw.SetColumnConfigs([]ColumnConfig{ {Number: 1, AutoMerge: true}, {Number: 2, AutoMerge: true}, {Number: 3, AutoMerge: true}, {Number: 4, AutoMerge: true}, {Number: 5, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter}, {Number: 6, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter}, }) tw.SetStyle(StyleLight) tw.Style().Options.SeparateRows = true expectedOut := `┌───┬─────────┬────────┬───────────┬───────────┬───────────┬─────┐ │ │ NODE IP │ PODS │ NAMESPACE │ CONTAINER │ RCE │ ID │ │ │ │ │ │ ├─────┬─────┼─────┤ │ │ │ │ │ │ EXE │ RUN │ │ ├───┼─────────┼────────┼───────────┼───────────┼─────┴─────┼─────┤ │ 1 │ 1.1.1.1 │ Pod 1A │ NS 1A │ C 1 │ Y │ 123 │ ├───┤ │ │ ├───────────┼─────┬─────┼─────┤ │ 2 │ │ │ │ C 2 │ Y │ N │ 234 │ ├───┤ │ ├───────────┼───────────┼─────┼─────┼─────┤ │ 3 │ │ │ NS 1B │ C 3 │ N │ N │ 345 │ ├───┤ ├────────┼───────────┼───────────┼─────┴─────┼─────┤ │ 4 │ │ Pod 1B │ NS 2 │ C 4 │ N │ 456 │ ├───┤ │ │ ├───────────┼─────┬─────┼─────┤ │ 5 │ │ │ │ C 5 │ Y │ N │ 567 │ ├───┼─────────┼────────┼───────────┼───────────┼─────┴─────┼─────┤ │ 6 │ 2.2.2.2 │ Pod 2 │ NS 3 │ C 6 │ Y │ 678 │ ├───┤ │ │ ├───────────┼───────────┼─────┤ │ 7 │ │ │ │ C 7 │ Y │ 789 │ ├───┼─────────┴────────┴───────────┼───────────┼───────────┼─────┤ │ │ │ 7 │ 5 │ │ │ │ │ ├─────┬─────┼─────┤ │ │ │ │ 5 │ 3 │ │ │ │ │ ├─────┴─────┼─────┤ │ │ │ │ 5 │ │ │ │ │ ├─────┬─────┼─────┤ │ │ │ │ 5 │ 3 │ │ │ │ │ ├─────┴─────┼─────┤ │ │ │ │ 5 │ │ └───┴──────────────────────────────┴───────────┴───────────┴─────┘` assert.Equal(t, expectedOut, tw.Render()) } func TestTable_Render_AutoMerge_ColumnsOnly(t *testing.T) { tw := NewWriter() tw.AppendHeader(Row{"Node IP", "Pods", "Namespace", "Container", "RCE\nEXE", "RCE\nRUN"}) tw.AppendRow(Row{"1.1.1.1", "Pod 1A", "NS 1A", "C 1", "Y", "Y"}) tw.AppendRow(Row{"1.1.1.1", "Pod 1A", "NS 1A", "C 2", "Y", "N"}) tw.AppendRow(Row{"1.1.1.1", "Pod 1A", "NS 1B", "C 3", "N", "N"}) tw.AppendRow(Row{"1.1.1.1", "Pod 1B", "NS 2", "C 4", "N", "N"}) tw.AppendRow(Row{"1.1.1.1", "Pod 1B", "NS 2", "C 5", "Y", "N"}) tw.AppendRow(Row{"2.2.2.2", "Pod 2", "NS 3", "C 6", "Y", "Y"}) tw.AppendRow(Row{"2.2.2.2", "Pod 2", "NS 3", "C 7", "Y", "Y"}) tw.AppendFooter(Row{"", "", "", 7, 5, 3}) tw.SetAutoIndex(true) tw.SetColumnConfigs([]ColumnConfig{ {Number: 1, AutoMerge: true}, {Number: 2, AutoMerge: true}, {Number: 3, AutoMerge: true}, {Number: 4, AutoMerge: true}, {Number: 5, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter}, {Number: 6, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter}, }) tw.SetStyle(StyleLight) tw.Style().Options.SeparateRows = true expectedOut := `┌───┬─────────┬────────┬───────────┬───────────┬─────┬─────┐ │ │ NODE IP │ PODS │ NAMESPACE │ CONTAINER │ RCE │ RCE │ │ │ │ │ │ │ EXE │ RUN │ ├───┼─────────┼────────┼───────────┼───────────┼─────┼─────┤ │ 1 │ 1.1.1.1 │ Pod 1A │ NS 1A │ C 1 │ Y │ Y │ ├───┤ │ │ ├───────────┼─────┼─────┤ │ 2 │ │ │ │ C 2 │ Y │ N │ ├───┤ │ ├───────────┼───────────┼─────┼─────┤ │ 3 │ │ │ NS 1B │ C 3 │ N │ N │ ├───┤ ├────────┼───────────┼───────────┼─────┼─────┤ │ 4 │ │ Pod 1B │ NS 2 │ C 4 │ N │ N │ ├───┤ │ │ ├───────────┼─────┼─────┤ │ 5 │ │ │ │ C 5 │ Y │ N │ ├───┼─────────┼────────┼───────────┼───────────┼─────┼─────┤ │ 6 │ 2.2.2.2 │ Pod 2 │ NS 3 │ C 6 │ Y │ Y │ ├───┤ │ │ ├───────────┼─────┼─────┤ │ 7 │ │ │ │ C 7 │ Y │ Y │ ├───┼─────────┼────────┼───────────┼───────────┼─────┼─────┤ │ │ │ │ │ 7 │ 5 │ 3 │ └───┴─────────┴────────┴───────────┴───────────┴─────┴─────┘` assert.Equal(t, expectedOut, tw.Render()) } func TestTable_Render_AutoMerge_RowsOnly(t *testing.T) { tw := NewWriter() tw.AppendHeader(Row{"Node IP", "Pods", "Namespace", "Container", "RCE", "RCE"}, RowConfig{AutoMerge: true}) tw.AppendHeader(Row{"", "", "", "", "EXE", "RUN"}) tw.AppendRow(Row{"1.1.1.1", "Pod 1A", "NS 1A", "C 1", "Y", "Y"}, RowConfig{AutoMerge: true}) tw.AppendRow(Row{"1.1.1.1", "Pod 1A", "NS 1A", "C 2", "Y", "N"}) tw.AppendRow(Row{"1.1.1.1", "Pod 1A", "NS 1B", "C 3", "N", "N"}) tw.AppendRow(Row{"1.1.1.1", "Pod 1B", "NS 2", "C 4", "N", "N"}, RowConfig{AutoMerge: true}) tw.AppendRow(Row{"1.1.1.1", "Pod 1B", "NS 2", "C 5", "Y", "N"}) tw.AppendRow(Row{"2.2.2.2", "Pod 2", "NS 3", "C 6", "Y", "Y"}, RowConfig{AutoMerge: true}) tw.AppendRow(Row{"2.2.2.2", "Pod 2", "NS 3", "C 7", "Y", "Y"}, RowConfig{AutoMerge: true}) tw.AppendFooter(Row{"", "", "", 7, 5, 3}) tw.SetAutoIndex(true) tw.SetColumnConfigs([]ColumnConfig{ {Number: 5, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter}, {Number: 6, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter}, }) tw.SetStyle(StyleLight) tw.Style().Options.SeparateRows = true expectedOut := `┌───┬─────────┬────────┬───────────┬───────────┬───────────┐ │ │ NODE IP │ PODS │ NAMESPACE │ CONTAINER │ RCE │ │ ├─────────┼────────┼───────────┼───────────┼─────┬─────┤ │ │ │ │ │ │ EXE │ RUN │ ├───┼─────────┼────────┼───────────┼───────────┼─────┴─────┤ │ 1 │ 1.1.1.1 │ Pod 1A │ NS 1A │ C 1 │ Y │ ├───┼─────────┼────────┼───────────┼───────────┼─────┬─────┤ │ 2 │ 1.1.1.1 │ Pod 1A │ NS 1A │ C 2 │ Y │ N │ ├───┼─────────┼────────┼───────────┼───────────┼─────┼─────┤ │ 3 │ 1.1.1.1 │ Pod 1A │ NS 1B │ C 3 │ N │ N │ ├───┼─────────┼────────┼───────────┼───────────┼─────┴─────┤ │ 4 │ 1.1.1.1 │ Pod 1B │ NS 2 │ C 4 │ N │ ├───┼─────────┼────────┼───────────┼───────────┼─────┬─────┤ │ 5 │ 1.1.1.1 │ Pod 1B │ NS 2 │ C 5 │ Y │ N │ ├───┼─────────┼────────┼───────────┼───────────┼─────┴─────┤ │ 6 │ 2.2.2.2 │ Pod 2 │ NS 3 │ C 6 │ Y │ ├───┼─────────┼────────┼───────────┼───────────┼───────────┤ │ 7 │ 2.2.2.2 │ Pod 2 │ NS 3 │ C 7 │ Y │ ├───┼─────────┼────────┼───────────┼───────────┼─────┬─────┤ │ │ │ │ │ 7 │ 5 │ 3 │ └───┴─────────┴────────┴───────────┴───────────┴─────┴─────┘` assert.Equal(t, expectedOut, tw.Render()) } func TestTable_Render_AutoMerge_WithHiddenRows(t *testing.T) { tw := NewWriter() tw.AppendHeader(Row{"Node IP", "Pods", "Namespace", "Container", "RCE\nEXE", "RCE\nRUN"}) tw.AppendRow(Row{"1.1.1.1", "Pod 1A", "NS 1A", "C 1", "Y", "Y"}) tw.AppendRow(Row{"1.1.1.1", "Pod 1A", "NS 1A", "C 2", "Y", "N"}) tw.AppendRow(Row{"1.1.1.1", "Pod 1A", "NS 1B", "C 3", "N", "N"}) tw.AppendRow(Row{"1.1.1.1", "Pod 1B", "NS 2", "C 4", "Y", "Y"}) tw.AppendRow(Row{"1.1.1.1", "Pod 1B", "NS 2", "C 5", "Y", "N"}) tw.AppendRow(Row{"2.2.2.2", "Pod 2", "NS 3", "C 6", "Y", "Y"}) tw.AppendRow(Row{"2.2.2.2", "Pod 2", "NS 3", "C 7", "Y", "N"}) tw.AppendFooter(Row{"", "", "", 7, 5, 3}) tw.SetColumnConfigs([]ColumnConfig{ {Number: 1, AutoMerge: true}, {Number: 2, AutoMerge: true}, {Number: 3, AutoMerge: true}, {Number: 4, Hidden: true}, {Number: 5, Hidden: true, Align: text.AlignCenter}, {Number: 6, Hidden: true, Align: text.AlignCenter}, }) tw.SetStyle(StyleLight) tw.Style().Options.SeparateRows = true expectedOut := `┌─────────┬────────┬───────────┐ │ NODE IP │ PODS │ NAMESPACE │ ├─────────┼────────┼───────────┤ │ 1.1.1.1 │ Pod 1A │ NS 1A │ │ │ │ │ │ │ │ │ │ │ ├───────────┤ │ │ │ NS 1B │ │ ├────────┼───────────┤ │ │ Pod 1B │ NS 2 │ │ │ │ │ │ │ │ │ ├─────────┼────────┼───────────┤ │ 2.2.2.2 │ Pod 2 │ NS 3 │ │ │ │ │ │ │ │ │ ├─────────┼────────┼───────────┤ │ │ │ │ └─────────┴────────┴───────────┘` assert.Equal(t, expectedOut, tw.Render()) } func TestTable_Render_BorderAndSeparators(t *testing.T) { table := Table{} table.AppendHeader(testHeader) table.AppendRows(testRows) table.AppendFooter(testFooter) expectedOut := `+-----+------------+-----------+--------+-----------------------------+ | # | FIRST NAME | LAST NAME | SALARY | | +-----+------------+-----------+--------+-----------------------------+ | 1 | Arya | Stark | 3000 | | | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | | 300 | Tyrion | Lannister | 5000 | | +-----+------------+-----------+--------+-----------------------------+ | | | TOTAL | 10000 | | +-----+------------+-----------+--------+-----------------------------+` assert.Equal(t, expectedOut, table.Render()) table.Style().Options = OptionsNoBorders expectedOut = ` # | FIRST NAME | LAST NAME | SALARY | -----+------------+-----------+--------+----------------------------- 1 | Arya | Stark | 3000 | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! 300 | Tyrion | Lannister | 5000 | -----+------------+-----------+--------+----------------------------- | | TOTAL | 10000 | ` assert.Equal(t, expectedOut, table.Render()) table.Style().Options.SeparateColumns = false expectedOut = ` # FIRST NAME LAST NAME SALARY ----------------------------------------------------------------- 1 Arya Stark 3000 20 Jon Snow 2000 You know nothing, Jon Snow! 300 Tyrion Lannister 5000 ----------------------------------------------------------------- TOTAL 10000 ` assert.Equal(t, expectedOut, table.Render()) table.Style().Options.SeparateFooter = false expectedOut = ` # FIRST NAME LAST NAME SALARY ----------------------------------------------------------------- 1 Arya Stark 3000 20 Jon Snow 2000 You know nothing, Jon Snow! 300 Tyrion Lannister 5000 TOTAL 10000 ` assert.Equal(t, expectedOut, table.Render()) table.Style().Options = OptionsNoBordersAndSeparators expectedOut = ` # FIRST NAME LAST NAME SALARY 1 Arya Stark 3000 20 Jon Snow 2000 You know nothing, Jon Snow! 300 Tyrion Lannister 5000 TOTAL 10000 ` assert.Equal(t, expectedOut, table.Render()) table.Style().Options.DrawBorder = true expectedOut = `+-----------------------------------------------------------------+ | # FIRST NAME LAST NAME SALARY | | 1 Arya Stark 3000 | | 20 Jon Snow 2000 You know nothing, Jon Snow! | | 300 Tyrion Lannister 5000 | | TOTAL 10000 | +-----------------------------------------------------------------+` assert.Equal(t, expectedOut, table.Render()) table.Style().Options.SeparateFooter = true expectedOut = `+-----------------------------------------------------------------+ | # FIRST NAME LAST NAME SALARY | | 1 Arya Stark 3000 | | 20 Jon Snow 2000 You know nothing, Jon Snow! | | 300 Tyrion Lannister 5000 | +-----------------------------------------------------------------+ | TOTAL 10000 | +-----------------------------------------------------------------+` assert.Equal(t, expectedOut, table.Render()) table.Style().Options.SeparateHeader = true expectedOut = `+-----------------------------------------------------------------+ | # FIRST NAME LAST NAME SALARY | +-----------------------------------------------------------------+ | 1 Arya Stark 3000 | | 20 Jon Snow 2000 You know nothing, Jon Snow! | | 300 Tyrion Lannister 5000 | +-----------------------------------------------------------------+ | TOTAL 10000 | +-----------------------------------------------------------------+` assert.Equal(t, expectedOut, table.Render()) table.Style().Options.SeparateRows = true expectedOut = `+-----------------------------------------------------------------+ | # FIRST NAME LAST NAME SALARY | +-----------------------------------------------------------------+ | 1 Arya Stark 3000 | +-----------------------------------------------------------------+ | 20 Jon Snow 2000 You know nothing, Jon Snow! | +-----------------------------------------------------------------+ | 300 Tyrion Lannister 5000 | +-----------------------------------------------------------------+ | TOTAL 10000 | +-----------------------------------------------------------------+` assert.Equal(t, expectedOut, table.Render()) table.Style().Options.SeparateColumns = true expectedOut = `+-----+------------+-----------+--------+-----------------------------+ | # | FIRST NAME | LAST NAME | SALARY | | +-----+------------+-----------+--------+-----------------------------+ | 1 | Arya | Stark | 3000 | | +-----+------------+-----------+--------+-----------------------------+ | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | +-----+------------+-----------+--------+-----------------------------+ | 300 | Tyrion | Lannister | 5000 | | +-----+------------+-----------+--------+-----------------------------+ | | | TOTAL | 10000 | | +-----+------------+-----------+--------+-----------------------------+` assert.Equal(t, expectedOut, table.Render()) } func TestTable_Render_Colored(t *testing.T) { tw := NewWriter() tw.AppendHeader(testHeader) tw.AppendRows(testRows) tw.AppendRow(testRowMultiLine) tw.AppendFooter(testFooter) tw.SetAutoIndex(true) tw.SetStyle(StyleColoredBright) tw.Style().Options.DrawBorder = true tw.Style().Options.SeparateColumns = true tw.Style().Options.SeparateFooter = true tw.Style().Options.SeparateHeader = true tw.Style().Options.SeparateRows = true expectedOut := []string{ "\x1b[106;30m+\x1b[0m\x1b[106;30m---\x1b[0m\x1b[106;30m+\x1b[0m\x1b[106;30m-----\x1b[0m\x1b[106;30m+\x1b[0m\x1b[106;30m------------\x1b[0m\x1b[106;30m+\x1b[0m\x1b[106;30m-----------\x1b[0m\x1b[106;30m+\x1b[0m\x1b[106;30m--------\x1b[0m\x1b[106;30m+\x1b[0m\x1b[106;30m-----------------------------\x1b[0m\x1b[106;30m+\x1b[0m", "\x1b[106;30m|\x1b[0m\x1b[106;30m \x1b[0m\x1b[106;30m|\x1b[0m\x1b[106;30m # \x1b[0m\x1b[106;30m|\x1b[0m\x1b[106;30m FIRST NAME \x1b[0m\x1b[106;30m|\x1b[0m\x1b[106;30m LAST NAME \x1b[0m\x1b[106;30m|\x1b[0m\x1b[106;30m SALARY \x1b[0m\x1b[106;30m|\x1b[0m\x1b[106;30m \x1b[0m\x1b[106;30m|\x1b[0m", "\x1b[106;30m+\x1b[0m\x1b[106;30m---\x1b[0m\x1b[106;30m+\x1b[0m\x1b[106;30m-----\x1b[0m\x1b[106;30m+\x1b[0m\x1b[106;30m------------\x1b[0m\x1b[106;30m+\x1b[0m\x1b[106;30m-----------\x1b[0m\x1b[106;30m+\x1b[0m\x1b[106;30m--------\x1b[0m\x1b[106;30m+\x1b[0m\x1b[106;30m-----------------------------\x1b[0m\x1b[106;30m+\x1b[0m", "\x1b[106;30m|\x1b[0m\x1b[106;30m 1 \x1b[0m\x1b[106;30m|\x1b[0m\x1b[107;30m 1 \x1b[0m\x1b[107;30m|\x1b[0m\x1b[107;30m Arya \x1b[0m\x1b[107;30m|\x1b[0m\x1b[107;30m Stark \x1b[0m\x1b[107;30m|\x1b[0m\x1b[107;30m 3000 \x1b[0m\x1b[107;30m|\x1b[0m\x1b[107;30m \x1b[0m\x1b[106;30m|\x1b[0m", "\x1b[106;30m+\x1b[0m\x1b[106;30m---\x1b[0m\x1b[106;30m+\x1b[0m\x1b[107;30m-----\x1b[0m\x1b[107;30m+\x1b[0m\x1b[107;30m------------\x1b[0m\x1b[107;30m+\x1b[0m\x1b[107;30m-----------\x1b[0m\x1b[107;30m+\x1b[0m\x1b[107;30m--------\x1b[0m\x1b[107;30m+\x1b[0m\x1b[107;30m-----------------------------\x1b[0m\x1b[106;30m+\x1b[0m", "\x1b[106;30m|\x1b[0m\x1b[106;30m 2 \x1b[0m\x1b[106;30m|\x1b[0m\x1b[47;30m 20 \x1b[0m\x1b[47;30m|\x1b[0m\x1b[47;30m Jon \x1b[0m\x1b[47;30m|\x1b[0m\x1b[47;30m Snow \x1b[0m\x1b[47;30m|\x1b[0m\x1b[47;30m 2000 \x1b[0m\x1b[47;30m|\x1b[0m\x1b[47;30m You know nothing, Jon Snow! \x1b[0m\x1b[106;30m|\x1b[0m", "\x1b[106;30m+\x1b[0m\x1b[106;30m---\x1b[0m\x1b[106;30m+\x1b[0m\x1b[47;30m-----\x1b[0m\x1b[47;30m+\x1b[0m\x1b[47;30m------------\x1b[0m\x1b[47;30m+\x1b[0m\x1b[47;30m-----------\x1b[0m\x1b[47;30m+\x1b[0m\x1b[47;30m--------\x1b[0m\x1b[47;30m+\x1b[0m\x1b[47;30m-----------------------------\x1b[0m\x1b[106;30m+\x1b[0m", "\x1b[106;30m|\x1b[0m\x1b[106;30m 3 \x1b[0m\x1b[106;30m|\x1b[0m\x1b[107;30m 300 \x1b[0m\x1b[107;30m|\x1b[0m\x1b[107;30m Tyrion \x1b[0m\x1b[107;30m|\x1b[0m\x1b[107;30m Lannister \x1b[0m\x1b[107;30m|\x1b[0m\x1b[107;30m 5000 \x1b[0m\x1b[107;30m|\x1b[0m\x1b[107;30m \x1b[0m\x1b[106;30m|\x1b[0m", "\x1b[106;30m+\x1b[0m\x1b[106;30m---\x1b[0m\x1b[106;30m+\x1b[0m\x1b[107;30m-----\x1b[0m\x1b[107;30m+\x1b[0m\x1b[107;30m------------\x1b[0m\x1b[107;30m+\x1b[0m\x1b[107;30m-----------\x1b[0m\x1b[107;30m+\x1b[0m\x1b[107;30m--------\x1b[0m\x1b[107;30m+\x1b[0m\x1b[107;30m-----------------------------\x1b[0m\x1b[106;30m+\x1b[0m", "\x1b[106;30m|\x1b[0m\x1b[106;30m 4 \x1b[0m\x1b[106;30m|\x1b[0m\x1b[47;30m 0 \x1b[0m\x1b[47;30m|\x1b[0m\x1b[47;30m Winter \x1b[0m\x1b[47;30m|\x1b[0m\x1b[47;30m Is \x1b[0m\x1b[47;30m|\x1b[0m\x1b[47;30m 0 \x1b[0m\x1b[47;30m|\x1b[0m\x1b[47;30m Coming. \x1b[0m\x1b[106;30m|\x1b[0m", "\x1b[106;30m|\x1b[0m\x1b[106;30m \x1b[0m\x1b[106;30m|\x1b[0m\x1b[47;30m \x1b[0m\x1b[47;30m|\x1b[0m\x1b[47;30m \x1b[0m\x1b[47;30m|\x1b[0m\x1b[47;30m \x1b[0m\x1b[47;30m|\x1b[0m\x1b[47;30m \x1b[0m\x1b[47;30m|\x1b[0m\x1b[47;30m The North Remembers! \x1b[0m\x1b[106;30m|\x1b[0m", "\x1b[106;30m|\x1b[0m\x1b[106;30m \x1b[0m\x1b[106;30m|\x1b[0m\x1b[47;30m \x1b[0m\x1b[47;30m|\x1b[0m\x1b[47;30m \x1b[0m\x1b[47;30m|\x1b[0m\x1b[47;30m \x1b[0m\x1b[47;30m|\x1b[0m\x1b[47;30m \x1b[0m\x1b[47;30m|\x1b[0m\x1b[47;30m This is known. \x1b[0m\x1b[106;30m|\x1b[0m", "\x1b[46;30m+\x1b[0m\x1b[46;30m---\x1b[0m\x1b[46;30m+\x1b[0m\x1b[46;30m-----\x1b[0m\x1b[46;30m+\x1b[0m\x1b[46;30m------------\x1b[0m\x1b[46;30m+\x1b[0m\x1b[46;30m-----------\x1b[0m\x1b[46;30m+\x1b[0m\x1b[46;30m--------\x1b[0m\x1b[46;30m+\x1b[0m\x1b[46;30m-----------------------------\x1b[0m\x1b[46;30m+\x1b[0m", "\x1b[46;30m|\x1b[0m\x1b[46;30m \x1b[0m\x1b[46;30m|\x1b[0m\x1b[46;30m \x1b[0m\x1b[46;30m|\x1b[0m\x1b[46;30m \x1b[0m\x1b[46;30m|\x1b[0m\x1b[46;30m TOTAL \x1b[0m\x1b[46;30m|\x1b[0m\x1b[46;30m 10000 \x1b[0m\x1b[46;30m|\x1b[0m\x1b[46;30m \x1b[0m\x1b[46;30m|\x1b[0m", "\x1b[46;30m+\x1b[0m\x1b[46;30m---\x1b[0m\x1b[46;30m+\x1b[0m\x1b[46;30m-----\x1b[0m\x1b[46;30m+\x1b[0m\x1b[46;30m------------\x1b[0m\x1b[46;30m+\x1b[0m\x1b[46;30m-----------\x1b[0m\x1b[46;30m+\x1b[0m\x1b[46;30m--------\x1b[0m\x1b[46;30m+\x1b[0m\x1b[46;30m-----------------------------\x1b[0m\x1b[46;30m+\x1b[0m", } out := tw.Render() assert.Equal(t, strings.Join(expectedOut, "\n"), out) if strings.Join(expectedOut, "\n") != out { for _, line := range strings.Split(out, "\n") { fmt.Printf("%#v,\n", line) } } } func TestTable_Render_ColoredCustom(t *testing.T) { tw := NewWriter() tw.AppendHeader(testHeader) tw.AppendRows(testRows) tw.AppendRow(testRowMultiLine) tw.AppendFooter(testFooter) tw.SetCaption(testCaption) tw.SetColumnConfigs([]ColumnConfig{ { Name: "#", Colors: testColor, ColorsHeader: testColorHiRedBold, }, { Name: "First Name", Colors: testColor, ColorsHeader: testColorHiRedBold, }, { Name: "Last Name", Colors: testColor, ColorsHeader: testColorHiRedBold, ColorsFooter: testColorHiBlueBold, }, { Name: "Salary", Colors: testColor, ColorsHeader: testColorHiRedBold, ColorsFooter: testColorHiBlueBold, }, { Number: 5, Colors: text.Colors{text.FgCyan}, }, }) tw.SetStyle(StyleRounded) expectedOut := []string{ "╭─────┬────────────┬───────────┬────────┬─────────────────────────────╮", "│\x1b[91;1m # \x1b[0m│\x1b[91;1m FIRST NAME \x1b[0m│\x1b[91;1m LAST NAME \x1b[0m│\x1b[91;1m SALARY \x1b[0m│ │", "├─────┼────────────┼───────────┼────────┼─────────────────────────────┤", "│\x1b[32m 1 \x1b[0m│\x1b[32m Arya \x1b[0m│\x1b[32m Stark \x1b[0m│\x1b[32m 3000 \x1b[0m│\x1b[36m \x1b[0m│", "│\x1b[32m 20 \x1b[0m│\x1b[32m Jon \x1b[0m│\x1b[32m Snow \x1b[0m│\x1b[32m 2000 \x1b[0m│\x1b[36m You know nothing, Jon Snow! \x1b[0m│", "│\x1b[32m 300 \x1b[0m│\x1b[32m Tyrion \x1b[0m│\x1b[32m Lannister \x1b[0m│\x1b[32m 5000 \x1b[0m│\x1b[36m \x1b[0m│", "│\x1b[32m 0 \x1b[0m│\x1b[32m Winter \x1b[0m│\x1b[32m Is \x1b[0m│\x1b[32m 0 \x1b[0m│\x1b[36m Coming. \x1b[0m│", "│\x1b[32m \x1b[0m│\x1b[32m \x1b[0m│\x1b[32m \x1b[0m│\x1b[32m \x1b[0m│\x1b[36m The North Remembers! \x1b[0m│", "│\x1b[32m \x1b[0m│\x1b[32m \x1b[0m│\x1b[32m \x1b[0m│\x1b[32m \x1b[0m│\x1b[36m This is known. \x1b[0m│", "├─────┼────────────┼───────────┼────────┼─────────────────────────────┤", "│ │ │\x1b[94;1m TOTAL \x1b[0m│\x1b[94;1m 10000 \x1b[0m│ │", "╰─────┴────────────┴───────────┴────────┴─────────────────────────────╯", "A Song of Ice and Fire", } assert.Equal(t, strings.Join(expectedOut, "\n"), tw.Render()) } func TestTable_Render_ColoredTableWithinATable(t *testing.T) { table := Table{} table.AppendHeader(testHeader) table.AppendRows(testRows) table.AppendFooter(testFooter) table.SetStyle(StyleColoredBright) table.SetIndexColumn(1) // colored is simple; render the colored table into another table tableOuter := Table{} tableOuter.AppendRow(Row{table.Render()}) tableOuter.SetStyle(StyleRounded) expectedOut := strings.Join([]string{ "╭───────────────────────────────────────────────────────────────────╮", "│ \x1b[106;30m # \x1b[0m\x1b[106;30m FIRST NAME \x1b[0m\x1b[106;30m LAST NAME \x1b[0m\x1b[106;30m SALARY \x1b[0m\x1b[106;30m \x1b[0m │", "│ \x1b[106;30m 1 \x1b[0m\x1b[107;30m Arya \x1b[0m\x1b[107;30m Stark \x1b[0m\x1b[107;30m 3000 \x1b[0m\x1b[107;30m \x1b[0m │", "│ \x1b[106;30m 20 \x1b[0m\x1b[47;30m Jon \x1b[0m\x1b[47;30m Snow \x1b[0m\x1b[47;30m 2000 \x1b[0m\x1b[47;30m You know nothing, Jon Snow! \x1b[0m │", "│ \x1b[106;30m 300 \x1b[0m\x1b[107;30m Tyrion \x1b[0m\x1b[107;30m Lannister \x1b[0m\x1b[107;30m 5000 \x1b[0m\x1b[107;30m \x1b[0m │", "│ \x1b[46;30m \x1b[0m\x1b[46;30m \x1b[0m\x1b[46;30m TOTAL \x1b[0m\x1b[46;30m 10000 \x1b[0m\x1b[46;30m \x1b[0m │", "╰───────────────────────────────────────────────────────────────────╯", }, "\n") out := tableOuter.Render() assert.Equal(t, expectedOut, out) // dump it out in a easy way to update the test if things are meant to // change due to some other feature if expectedOut != out { for _, line := range strings.Split(out, "\n") { fmt.Printf("%#v,\n", line) } fmt.Println() } } func TestTable_Render_ColoredTableWithinAColoredTable(t *testing.T) { table := Table{} table.AppendHeader(testHeader) table.AppendRows(testRows) table.AppendFooter(testFooter) table.SetStyle(StyleColoredBright) table.SetIndexColumn(1) // colored is simple; render the colored table into another colored table tableOuter := Table{} tableOuter.AppendHeader(Row{"Colored Table within a Colored Table"}) tableOuter.AppendRow(Row{"\n" + table.Render() + "\n"}) tableOuter.SetColumnConfigs([]ColumnConfig{{Number: 1, AlignHeader: text.AlignCenter}}) tableOuter.SetStyle(StyleColoredBright) expectedOut := strings.Join([]string{ "\x1b[106;30m COLORED TABLE WITHIN A COLORED TABLE \x1b[0m", "\x1b[107;30m \x1b[0m", "\x1b[107;30m \x1b[106;30m # \x1b[0m\x1b[107;30m\x1b[106;30m FIRST NAME \x1b[0m\x1b[107;30m\x1b[106;30m LAST NAME \x1b[0m\x1b[107;30m\x1b[106;30m SALARY \x1b[0m\x1b[107;30m\x1b[106;30m \x1b[0m\x1b[107;30m \x1b[0m", "\x1b[107;30m \x1b[106;30m 1 \x1b[0m\x1b[107;30m\x1b[107;30m Arya \x1b[0m\x1b[107;30m\x1b[107;30m Stark \x1b[0m\x1b[107;30m\x1b[107;30m 3000 \x1b[0m\x1b[107;30m\x1b[107;30m \x1b[0m\x1b[107;30m \x1b[0m", "\x1b[107;30m \x1b[106;30m 20 \x1b[0m\x1b[107;30m\x1b[47;30m Jon \x1b[0m\x1b[107;30m\x1b[47;30m Snow \x1b[0m\x1b[107;30m\x1b[47;30m 2000 \x1b[0m\x1b[107;30m\x1b[47;30m You know nothing, Jon Snow! \x1b[0m\x1b[107;30m \x1b[0m", "\x1b[107;30m \x1b[106;30m 300 \x1b[0m\x1b[107;30m\x1b[107;30m Tyrion \x1b[0m\x1b[107;30m\x1b[107;30m Lannister \x1b[0m\x1b[107;30m\x1b[107;30m 5000 \x1b[0m\x1b[107;30m\x1b[107;30m \x1b[0m\x1b[107;30m \x1b[0m", "\x1b[107;30m \x1b[46;30m \x1b[0m\x1b[107;30m\x1b[46;30m \x1b[0m\x1b[107;30m\x1b[46;30m TOTAL \x1b[0m\x1b[107;30m\x1b[46;30m 10000 \x1b[0m\x1b[107;30m\x1b[46;30m \x1b[0m\x1b[107;30m \x1b[0m", "\x1b[107;30m \x1b[0m", }, "\n") out := tableOuter.Render() assert.Equal(t, expectedOut, out) // dump it out in a easy way to update the test if things are meant to // change due to some other feature if expectedOut != out { for _, line := range strings.Split(out, "\n") { fmt.Printf("%#v,\n", line) } fmt.Println() } } func TestTable_Render_ColoredStyleAutoIndex(t *testing.T) { table := Table{} table.AppendHeader(testHeader) table.AppendRows(testRows) table.AppendFooter(testFooter) table.SetAutoIndex(true) table.SetStyle(StyleColoredDark) table.SetTitle(testTitle2) expectedOut := strings.Join([]string{ "\x1b[106;30;1m When you play the Game of Thrones, you win or you die. There is no \x1b[0m", "\x1b[106;30;1m middle ground. \x1b[0m", "\x1b[96;100m \x1b[0m\x1b[96;100m # \x1b[0m\x1b[96;100m FIRST NAME \x1b[0m\x1b[96;100m LAST NAME \x1b[0m\x1b[96;100m SALARY \x1b[0m\x1b[96;100m \x1b[0m", "\x1b[96;100m 1 \x1b[0m\x1b[97;40m 1 \x1b[0m\x1b[97;40m Arya \x1b[0m\x1b[97;40m Stark \x1b[0m\x1b[97;40m 3000 \x1b[0m\x1b[97;40m \x1b[0m", "\x1b[96;100m 2 \x1b[0m\x1b[37;40m 20 \x1b[0m\x1b[37;40m Jon \x1b[0m\x1b[37;40m Snow \x1b[0m\x1b[37;40m 2000 \x1b[0m\x1b[37;40m You know nothing, Jon Snow! \x1b[0m", "\x1b[96;100m 3 \x1b[0m\x1b[97;40m 300 \x1b[0m\x1b[97;40m Tyrion \x1b[0m\x1b[97;40m Lannister \x1b[0m\x1b[97;40m 5000 \x1b[0m\x1b[97;40m \x1b[0m", "\x1b[36;100m \x1b[0m\x1b[36;100m \x1b[0m\x1b[36;100m \x1b[0m\x1b[36;100m TOTAL \x1b[0m\x1b[36;100m 10000 \x1b[0m\x1b[36;100m \x1b[0m", }, "\n") out := table.Render() assert.Equal(t, expectedOut, out) // dump it out in a easy way to update the test if things are meant to // change due to some other feature if expectedOut != out { for _, line := range strings.Split(out, "\n") { fmt.Printf("%#v,\n", line) } fmt.Println() } } func TestTable_Render_ColumnConfigs(t *testing.T) { generatePrefixTransformer := func(prefix string) text.Transformer { return func(val interface{}) string { return fmt.Sprintf("%s%v", prefix, val) } } generateSuffixTransformer := func(suffix string) text.Transformer { return func(val interface{}) string { return fmt.Sprintf("%v%s", val, suffix) } } salaryTransformer := text.Transformer(func(val interface{}) string { if valInt, ok := val.(int); ok { return fmt.Sprintf("$ %.2f", float64(valInt)+0.03) } return strings.Replace(fmt.Sprint(val), "ry", "riii", -1) }) tw := NewWriter() tw.AppendHeader(testHeaderMultiLine) tw.AppendRows(testRows) tw.AppendRow(testRowMultiLine) tw.AppendFooter(testFooterMultiLine) tw.SetAutoIndex(true) tw.SetColumnConfigs([]ColumnConfig{ { Name: fmt.Sprint(testHeaderMultiLine[1]), // First Name Align: text.AlignRight, AlignFooter: text.AlignRight, AlignHeader: text.AlignRight, Colors: text.Colors{text.BgBlack, text.FgRed}, ColorsHeader: text.Colors{text.BgRed, text.FgBlack, text.Bold}, ColorsFooter: text.Colors{text.BgRed, text.FgBlack}, Transformer: generatePrefixTransformer("(r_"), TransformerFooter: generatePrefixTransformer("(f_"), TransformerHeader: generatePrefixTransformer("(h_"), VAlign: text.VAlignTop, VAlignFooter: text.VAlignTop, VAlignHeader: text.VAlignTop, WidthMax: 10, }, { Name: fmt.Sprint(testHeaderMultiLine[2]), // Last Name Align: text.AlignLeft, AlignFooter: text.AlignLeft, AlignHeader: text.AlignLeft, Colors: text.Colors{text.BgBlack, text.FgGreen}, ColorsHeader: text.Colors{text.BgGreen, text.FgBlack, text.Bold}, ColorsFooter: text.Colors{text.BgGreen, text.FgBlack}, Transformer: generateSuffixTransformer("_r)"), TransformerFooter: generateSuffixTransformer("_f)"), TransformerHeader: generateSuffixTransformer("_h)"), VAlign: text.VAlignMiddle, VAlignFooter: text.VAlignMiddle, VAlignHeader: text.VAlignMiddle, WidthMax: 10, }, { Number: 4, // Salary Colors: text.Colors{text.BgBlack, text.FgBlue}, ColorsHeader: text.Colors{text.BgBlue, text.FgBlack, text.Bold}, ColorsFooter: text.Colors{text.BgBlue, text.FgBlack}, Transformer: salaryTransformer, TransformerFooter: salaryTransformer, TransformerHeader: salaryTransformer, VAlign: text.VAlignBottom, VAlignFooter: text.VAlignBottom, VAlignHeader: text.VAlignBottom, WidthMin: 16, }, { Name: "Non-existent Column", Colors: text.Colors{text.BgYellow, text.FgHiRed}, }, }) tw.SetStyle(styleTest) expectedOutLines := []string{ "(---^-----^-----------^------------^------------------^-----------------------------)", "[< >|< #>|\x1b[41;30;1m< (H_FIRST>\x1b[0m|\x1b[42;30;1m\x1b[0m|\x1b[44;30;1m< >\x1b[0m|< >]", "[< >|< >|\x1b[41;30;1m< NAME>\x1b[0m|\x1b[42;30;1m\x1b[0m|\x1b[44;30;1m< SALARIII>\x1b[0m|< >]", "{---+-----+-----------+------------+------------------+-----------------------------}", "[<1>|< 1>|\x1b[40;31m< (r_Arya>\x1b[0m|\x1b[40;32m\x1b[0m|\x1b[40;34m< $ 3000.03>\x1b[0m|< >]", "[<2>|< 20>|\x1b[40;31m< (r_Jon>\x1b[0m|\x1b[40;32m\x1b[0m|\x1b[40;34m< $ 2000.03>\x1b[0m|]", "[<3>|<300>|\x1b[40;31m<(r_Tyrion>\x1b[0m|\x1b[40;32m\x1b[0m|\x1b[40;34m< >\x1b[0m|< >]", "[< >|< >|\x1b[40;31m< >\x1b[0m|\x1b[40;32m\x1b[0m|\x1b[40;34m< $ 5000.03>\x1b[0m|< >]", "[<4>|< 0>|\x1b[40;31m<(r_Winter>\x1b[0m|\x1b[40;32m< >\x1b[0m|\x1b[40;34m< >\x1b[0m|]", "[< >|< >|\x1b[40;31m< >\x1b[0m|\x1b[40;32m\x1b[0m|\x1b[40;34m< >\x1b[0m|]", "[< >|< >|\x1b[40;31m< >\x1b[0m|\x1b[40;32m< >\x1b[0m|\x1b[40;34m< $ 0.03>\x1b[0m|]", "{---+-----+-----------+------------+------------------+-----------------------------}", "[< >|< >|\x1b[41;30m< (F_>\x1b[0m|\x1b[42;30m\x1b[0m|\x1b[44;30m< >\x1b[0m|< >]", "[< >|< >|\x1b[41;30m< >\x1b[0m|\x1b[42;30m\x1b[0m|\x1b[44;30m< $ 10000.03>\x1b[0m|< >]", "\\---v-----v-----------v------------v------------------v-----------------------------/", } expectedOut := strings.Join(expectedOutLines, "\n") assert.Equal(t, expectedOut, tw.Render()) } func TestTable_Render_Empty(t *testing.T) { tw := NewWriter() assert.Empty(t, tw.Render()) } func TestTable_Render_HiddenColumns(t *testing.T) { tw := NewWriter() tw.AppendHeader(testHeader) tw.AppendRows(testRows) tw.AppendFooter(testFooter) // ensure sorting is done before hiding the columns tw.SortBy([]SortBy{ {Name: "Salary", Mode: DscNumeric}, }) t.Run("no columns hidden", func(t *testing.T) { tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns(nil)) expectedOut := `+-----+------------+-------------+--------+-------------------------------+ | # | FIRST NAME | LAST NAME | SALARY | | +-----+------------+-------------+--------+-------------------------------+ | 307 | >>Tyrion | Lannister<< | 5013 | | | 8 | >>Arya | Stark<< | 3013 | | | 27 | >>Jon | Snow<< | 2013 | ~You know nothing, Jon Snow!~ | +-----+------------+-------------+--------+-------------------------------+ | | | TOTAL | 10000 | | +-----+------------+-------------+--------+-------------------------------+` assert.Equal(t, expectedOut, tw.Render()) }) t.Run("every column hidden", func(t *testing.T) { tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{0, 1, 2, 3, 4})) expectedOut := `` assert.Equal(t, expectedOut, tw.Render()) }) t.Run("some columns hidden (1)", func(t *testing.T) { tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{1, 2, 3, 4})) expectedOut := `+-----+ | # | +-----+ | 307 | | 8 | | 27 | +-----+ | | +-----+` assert.Equal(t, expectedOut, tw.Render()) }) t.Run("some columns hidden (2)", func(t *testing.T) { tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{1, 2, 3})) expectedOut := `+-----+-------------------------------+ | # | | +-----+-------------------------------+ | 307 | | | 8 | | | 27 | ~You know nothing, Jon Snow!~ | +-----+-------------------------------+ | | | +-----+-------------------------------+` assert.Equal(t, expectedOut, tw.Render()) }) t.Run("some columns hidden (3)", func(t *testing.T) { tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{0, 4})) expectedOut := `+------------+-------------+--------+ | FIRST NAME | LAST NAME | SALARY | +------------+-------------+--------+ | >>Tyrion | Lannister<< | 5013 | | >>Arya | Stark<< | 3013 | | >>Jon | Snow<< | 2013 | +------------+-------------+--------+ | | TOTAL | 10000 | +------------+-------------+--------+` assert.Equal(t, expectedOut, tw.Render()) }) t.Run("first column hidden", func(t *testing.T) { tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{0})) expectedOut := `+------------+-------------+--------+-------------------------------+ | FIRST NAME | LAST NAME | SALARY | | +------------+-------------+--------+-------------------------------+ | >>Tyrion | Lannister<< | 5013 | | | >>Arya | Stark<< | 3013 | | | >>Jon | Snow<< | 2013 | ~You know nothing, Jon Snow!~ | +------------+-------------+--------+-------------------------------+ | | TOTAL | 10000 | | +------------+-------------+--------+-------------------------------+` assert.Equal(t, expectedOut, tw.Render()) }) t.Run("column hidden in the middle", func(t *testing.T) { tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{1})) expectedOut := `+-----+-------------+--------+-------------------------------+ | # | LAST NAME | SALARY | | +-----+-------------+--------+-------------------------------+ | 307 | Lannister<< | 5013 | | | 8 | Stark<< | 3013 | | | 27 | Snow<< | 2013 | ~You know nothing, Jon Snow!~ | +-----+-------------+--------+-------------------------------+ | | TOTAL | 10000 | | +-----+-------------+--------+-------------------------------+` assert.Equal(t, expectedOut, tw.Render()) }) t.Run("last column hidden", func(t *testing.T) { tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{4})) expectedOut := `+-----+------------+-------------+--------+ | # | FIRST NAME | LAST NAME | SALARY | +-----+------------+-------------+--------+ | 307 | >>Tyrion | Lannister<< | 5013 | | 8 | >>Arya | Stark<< | 3013 | | 27 | >>Jon | Snow<< | 2013 | +-----+------------+-------------+--------+ | | | TOTAL | 10000 | +-----+------------+-------------+--------+` assert.Equal(t, expectedOut, tw.Render()) }) } func TestTable_Render_Paged(t *testing.T) { tw := NewWriter() tw.AppendHeader(testHeader) tw.AppendRows(testRows) tw.AppendRow(testRowMultiLine) tw.AppendFooter(Row{"", "", "Total", 10000}) tw.SetPageSize(1) expectedOut := `+-----+------------+-----------+--------+-----------------------------+ | # | FIRST NAME | LAST NAME | SALARY | | +-----+------------+-----------+--------+-----------------------------+ | 1 | Arya | Stark | 3000 | | +-----+------------+-----------+--------+-----------------------------+ | | | TOTAL | 10000 | | +-----+------------+-----------+--------+-----------------------------+ +-----+------------+-----------+--------+-----------------------------+ | # | FIRST NAME | LAST NAME | SALARY | | +-----+------------+-----------+--------+-----------------------------+ | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | +-----+------------+-----------+--------+-----------------------------+ | | | TOTAL | 10000 | | +-----+------------+-----------+--------+-----------------------------+ +-----+------------+-----------+--------+-----------------------------+ | # | FIRST NAME | LAST NAME | SALARY | | +-----+------------+-----------+--------+-----------------------------+ | 300 | Tyrion | Lannister | 5000 | | +-----+------------+-----------+--------+-----------------------------+ | | | TOTAL | 10000 | | +-----+------------+-----------+--------+-----------------------------+ +-----+------------+-----------+--------+-----------------------------+ | # | FIRST NAME | LAST NAME | SALARY | | +-----+------------+-----------+--------+-----------------------------+ | 0 | Winter | Is | 0 | Coming. | +-----+------------+-----------+--------+-----------------------------+ | | | TOTAL | 10000 | | +-----+------------+-----------+--------+-----------------------------+ +-----+------------+-----------+--------+-----------------------------+ | # | FIRST NAME | LAST NAME | SALARY | | +-----+------------+-----------+--------+-----------------------------+ | | | | | The North Remembers! | +-----+------------+-----------+--------+-----------------------------+ | | | TOTAL | 10000 | | +-----+------------+-----------+--------+-----------------------------+ +-----+------------+-----------+--------+-----------------------------+ | # | FIRST NAME | LAST NAME | SALARY | | +-----+------------+-----------+--------+-----------------------------+ | | | | | This is known. | +-----+------------+-----------+--------+-----------------------------+ | | | TOTAL | 10000 | | +-----+------------+-----------+--------+-----------------------------+` assert.Equal(t, expectedOut, tw.Render()) } func TestTable_Render_Reset(t *testing.T) { tw := NewWriter() tw.AppendHeader(testHeader) tw.AppendRows(testRows) tw.AppendFooter(testFooter) tw.SetStyle(StyleLight) expectedOut := `┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐ │ # │ FIRST NAME │ LAST NAME │ SALARY │ │ ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ │ 1 │ Arya │ Stark │ 3000 │ │ │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ │ 300 │ Tyrion │ Lannister │ 5000 │ │ ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ │ │ │ TOTAL │ 10000 │ │ └─────┴────────────┴───────────┴────────┴─────────────────────────────┘` assert.Equal(t, expectedOut, tw.Render()) tw.ResetFooters() expectedOut = `┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐ │ # │ FIRST NAME │ LAST NAME │ SALARY │ │ ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ │ 1 │ Arya │ Stark │ 3000 │ │ │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ │ 300 │ Tyrion │ Lannister │ 5000 │ │ └─────┴────────────┴───────────┴────────┴─────────────────────────────┘` assert.Equal(t, expectedOut, tw.Render()) tw.ResetHeaders() expectedOut = `┌─────┬────────┬───────────┬──────┬─────────────────────────────┐ │ 1 │ Arya │ Stark │ 3000 │ │ │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ │ 300 │ Tyrion │ Lannister │ 5000 │ │ └─────┴────────┴───────────┴──────┴─────────────────────────────┘` assert.Equal(t, expectedOut, tw.Render()) tw.ResetRows() assert.Empty(t, tw.Render()) } func TestTable_Render_RowPainter(t *testing.T) { tw := NewWriter() tw.AppendHeader(testHeader) tw.AppendRows(testRows) tw.AppendRow(testRowMultiLine) tw.AppendFooter(testFooter) tw.SetIndexColumn(1) tw.SetRowPainter(RowPainter(func(row Row) text.Colors { if salary, ok := row[3].(int); ok { if salary > 3000 { return text.Colors{text.BgYellow, text.FgBlack} } else if salary < 2000 { return text.Colors{text.BgRed, text.FgBlack} } } return nil })) tw.SetStyle(StyleLight) tw.SortBy([]SortBy{{Name: "Salary", Mode: AscNumeric}}) expectedOutLines := []string{ "┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐", "│ # │ FIRST NAME │ LAST NAME │ SALARY │ │", "├─────┼────────────┼───────────┼────────┼─────────────────────────────┤", "│ 0 │\x1b[41;30m Winter \x1b[0m│\x1b[41;30m Is \x1b[0m│\x1b[41;30m 0 \x1b[0m│\x1b[41;30m Coming. \x1b[0m│", "│ │\x1b[41;30m \x1b[0m│\x1b[41;30m \x1b[0m│\x1b[41;30m \x1b[0m│\x1b[41;30m The North Remembers! \x1b[0m│", "│ │\x1b[41;30m \x1b[0m│\x1b[41;30m \x1b[0m│\x1b[41;30m \x1b[0m│\x1b[41;30m This is known. \x1b[0m│", "│ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │", "│ 1 │ Arya │ Stark │ 3000 │ │", "│ 300 │\x1b[43;30m Tyrion \x1b[0m│\x1b[43;30m Lannister \x1b[0m│\x1b[43;30m 5000 \x1b[0m│\x1b[43;30m \x1b[0m│", "├─────┼────────────┼───────────┼────────┼─────────────────────────────┤", "│ │ │ TOTAL │ 10000 │ │", "└─────┴────────────┴───────────┴────────┴─────────────────────────────┘", } expectedOut := strings.Join(expectedOutLines, "\n") assert.Equal(t, expectedOut, tw.Render()) tw.SetStyle(StyleColoredBright) tw.Style().Color.RowAlternate = tw.Style().Color.Row expectedOutLines = []string{ "\x1b[106;30m # \x1b[0m\x1b[106;30m FIRST NAME \x1b[0m\x1b[106;30m LAST NAME \x1b[0m\x1b[106;30m SALARY \x1b[0m\x1b[106;30m \x1b[0m", "\x1b[106;30m 0 \x1b[0m\x1b[41;30m Winter \x1b[0m\x1b[41;30m Is \x1b[0m\x1b[41;30m 0 \x1b[0m\x1b[41;30m Coming. \x1b[0m", "\x1b[106;30m \x1b[0m\x1b[41;30m \x1b[0m\x1b[41;30m \x1b[0m\x1b[41;30m \x1b[0m\x1b[41;30m The North Remembers! \x1b[0m", "\x1b[106;30m \x1b[0m\x1b[41;30m \x1b[0m\x1b[41;30m \x1b[0m\x1b[41;30m \x1b[0m\x1b[41;30m This is known. \x1b[0m", "\x1b[106;30m 20 \x1b[0m\x1b[107;30m Jon \x1b[0m\x1b[107;30m Snow \x1b[0m\x1b[107;30m 2000 \x1b[0m\x1b[107;30m You know nothing, Jon Snow! \x1b[0m", "\x1b[106;30m 1 \x1b[0m\x1b[107;30m Arya \x1b[0m\x1b[107;30m Stark \x1b[0m\x1b[107;30m 3000 \x1b[0m\x1b[107;30m \x1b[0m", "\x1b[106;30m 300 \x1b[0m\x1b[43;30m Tyrion \x1b[0m\x1b[43;30m Lannister \x1b[0m\x1b[43;30m 5000 \x1b[0m\x1b[43;30m \x1b[0m", "\x1b[46;30m \x1b[0m\x1b[46;30m \x1b[0m\x1b[46;30m TOTAL \x1b[0m\x1b[46;30m 10000 \x1b[0m\x1b[46;30m \x1b[0m", } expectedOut = strings.Join(expectedOutLines, "\n") assert.Equal(t, expectedOut, tw.Render()) } func TestTable_Render_Sorted(t *testing.T) { tw := NewWriter() tw.AppendHeader(testHeader) tw.AppendRows(testRows) tw.AppendRow(Row{11, "Sansa", "Stark", 6000}) tw.AppendFooter(testFooter) tw.SetStyle(StyleLight) tw.SortBy([]SortBy{{Name: "Last Name", Mode: Asc}, {Name: "First Name", Mode: Asc}}) expectedOut := `┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐ │ # │ FIRST NAME │ LAST NAME │ SALARY │ │ ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ │ 300 │ Tyrion │ Lannister │ 5000 │ │ │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ │ 1 │ Arya │ Stark │ 3000 │ │ │ 11 │ Sansa │ Stark │ 6000 │ │ ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ │ │ │ TOTAL │ 10000 │ │ └─────┴────────────┴───────────┴────────┴─────────────────────────────┘` assert.Equal(t, expectedOut, tw.Render()) } func TestTable_Render_Separator(t *testing.T) { tw := NewWriter() tw.AppendHeader(testHeader) tw.AppendSeparator() // doesn't make any difference tw.AppendRows(testRows) tw.AppendSeparator() tw.AppendSeparator() // doesn't make any difference tw.AppendRow(testRowMultiLine) tw.AppendSeparator() tw.AppendSeparator() // doesn't make any difference tw.AppendSeparator() // doesn't make any difference tw.AppendRow(Row{11, "Sansa", "Stark", 6000}) tw.AppendSeparator() // doesn't make any difference tw.AppendSeparator() // doesn't make any difference tw.AppendSeparator() // doesn't make any difference tw.AppendSeparator() // doesn't make any difference tw.AppendFooter(testFooter) tw.SetStyle(StyleLight) expectedOut := `┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐ │ # │ FIRST NAME │ LAST NAME │ SALARY │ │ ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ │ 1 │ Arya │ Stark │ 3000 │ │ │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ │ 300 │ Tyrion │ Lannister │ 5000 │ │ ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ │ 0 │ Winter │ Is │ 0 │ Coming. │ │ │ │ │ │ The North Remembers! │ │ │ │ │ │ This is known. │ ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ │ 11 │ Sansa │ Stark │ 6000 │ │ ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ │ │ │ TOTAL │ 10000 │ │ └─────┴────────────┴───────────┴────────┴─────────────────────────────┘` assert.Equal(t, expectedOut, tw.Render()) } func TestTable_Render_Styles(t *testing.T) { tw := NewWriter() tw.AppendHeader(testHeader) tw.AppendRows(testRows) tw.AppendFooter(testFooter) tw.SetStyle(StyleLight) styles := map[*Style]string{ &StyleDefault: "+-----+------------+-----------+--------+-----------------------------+\n| # | FIRST NAME | LAST NAME | SALARY | |\n+-----+------------+-----------+--------+-----------------------------+\n| 1 | Arya | Stark | 3000 | |\n| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |\n| 300 | Tyrion | Lannister | 5000 | |\n+-----+------------+-----------+--------+-----------------------------+\n| | | TOTAL | 10000 | |\n+-----+------------+-----------+--------+-----------------------------+", &StyleBold: "┏━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n┃ # ┃ FIRST NAME ┃ LAST NAME ┃ SALARY ┃ ┃\n┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫\n┃ 1 ┃ Arya ┃ Stark ┃ 3000 ┃ ┃\n┃ 20 ┃ Jon ┃ Snow ┃ 2000 ┃ You know nothing, Jon Snow! ┃\n┃ 300 ┃ Tyrion ┃ Lannister ┃ 5000 ┃ ┃\n┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫\n┃ ┃ ┃ TOTAL ┃ 10000 ┃ ┃\n┗━━━━━┻━━━━━━━━━━━━┻━━━━━━━━━━━┻━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛", &StyleColoredBlackOnBlueWhite: "\x1b[104;30m # \x1b[0m\x1b[104;30m FIRST NAME \x1b[0m\x1b[104;30m LAST NAME \x1b[0m\x1b[104;30m SALARY \x1b[0m\x1b[104;30m \x1b[0m\n\x1b[107;30m 1 \x1b[0m\x1b[107;30m Arya \x1b[0m\x1b[107;30m Stark \x1b[0m\x1b[107;30m 3000 \x1b[0m\x1b[107;30m \x1b[0m\n\x1b[47;30m 20 \x1b[0m\x1b[47;30m Jon \x1b[0m\x1b[47;30m Snow \x1b[0m\x1b[47;30m 2000 \x1b[0m\x1b[47;30m You know nothing, Jon Snow! \x1b[0m\n\x1b[107;30m 300 \x1b[0m\x1b[107;30m Tyrion \x1b[0m\x1b[107;30m Lannister \x1b[0m\x1b[107;30m 5000 \x1b[0m\x1b[107;30m \x1b[0m\n\x1b[44;30m \x1b[0m\x1b[44;30m \x1b[0m\x1b[44;30m TOTAL \x1b[0m\x1b[44;30m 10000 \x1b[0m\x1b[44;30m \x1b[0m", &StyleColoredBlackOnCyanWhite: "\x1b[106;30m # \x1b[0m\x1b[106;30m FIRST NAME \x1b[0m\x1b[106;30m LAST NAME \x1b[0m\x1b[106;30m SALARY \x1b[0m\x1b[106;30m \x1b[0m\n\x1b[107;30m 1 \x1b[0m\x1b[107;30m Arya \x1b[0m\x1b[107;30m Stark \x1b[0m\x1b[107;30m 3000 \x1b[0m\x1b[107;30m \x1b[0m\n\x1b[47;30m 20 \x1b[0m\x1b[47;30m Jon \x1b[0m\x1b[47;30m Snow \x1b[0m\x1b[47;30m 2000 \x1b[0m\x1b[47;30m You know nothing, Jon Snow! \x1b[0m\n\x1b[107;30m 300 \x1b[0m\x1b[107;30m Tyrion \x1b[0m\x1b[107;30m Lannister \x1b[0m\x1b[107;30m 5000 \x1b[0m\x1b[107;30m \x1b[0m\n\x1b[46;30m \x1b[0m\x1b[46;30m \x1b[0m\x1b[46;30m TOTAL \x1b[0m\x1b[46;30m 10000 \x1b[0m\x1b[46;30m \x1b[0m", &StyleColoredBlackOnGreenWhite: "\x1b[102;30m # \x1b[0m\x1b[102;30m FIRST NAME \x1b[0m\x1b[102;30m LAST NAME \x1b[0m\x1b[102;30m SALARY \x1b[0m\x1b[102;30m \x1b[0m\n\x1b[107;30m 1 \x1b[0m\x1b[107;30m Arya \x1b[0m\x1b[107;30m Stark \x1b[0m\x1b[107;30m 3000 \x1b[0m\x1b[107;30m \x1b[0m\n\x1b[47;30m 20 \x1b[0m\x1b[47;30m Jon \x1b[0m\x1b[47;30m Snow \x1b[0m\x1b[47;30m 2000 \x1b[0m\x1b[47;30m You know nothing, Jon Snow! \x1b[0m\n\x1b[107;30m 300 \x1b[0m\x1b[107;30m Tyrion \x1b[0m\x1b[107;30m Lannister \x1b[0m\x1b[107;30m 5000 \x1b[0m\x1b[107;30m \x1b[0m\n\x1b[42;30m \x1b[0m\x1b[42;30m \x1b[0m\x1b[42;30m TOTAL \x1b[0m\x1b[42;30m 10000 \x1b[0m\x1b[42;30m \x1b[0m", &StyleColoredBlackOnMagentaWhite: "\x1b[105;30m # \x1b[0m\x1b[105;30m FIRST NAME \x1b[0m\x1b[105;30m LAST NAME \x1b[0m\x1b[105;30m SALARY \x1b[0m\x1b[105;30m \x1b[0m\n\x1b[107;30m 1 \x1b[0m\x1b[107;30m Arya \x1b[0m\x1b[107;30m Stark \x1b[0m\x1b[107;30m 3000 \x1b[0m\x1b[107;30m \x1b[0m\n\x1b[47;30m 20 \x1b[0m\x1b[47;30m Jon \x1b[0m\x1b[47;30m Snow \x1b[0m\x1b[47;30m 2000 \x1b[0m\x1b[47;30m You know nothing, Jon Snow! \x1b[0m\n\x1b[107;30m 300 \x1b[0m\x1b[107;30m Tyrion \x1b[0m\x1b[107;30m Lannister \x1b[0m\x1b[107;30m 5000 \x1b[0m\x1b[107;30m \x1b[0m\n\x1b[45;30m \x1b[0m\x1b[45;30m \x1b[0m\x1b[45;30m TOTAL \x1b[0m\x1b[45;30m 10000 \x1b[0m\x1b[45;30m \x1b[0m", &StyleColoredBlackOnRedWhite: "\x1b[101;30m # \x1b[0m\x1b[101;30m FIRST NAME \x1b[0m\x1b[101;30m LAST NAME \x1b[0m\x1b[101;30m SALARY \x1b[0m\x1b[101;30m \x1b[0m\n\x1b[107;30m 1 \x1b[0m\x1b[107;30m Arya \x1b[0m\x1b[107;30m Stark \x1b[0m\x1b[107;30m 3000 \x1b[0m\x1b[107;30m \x1b[0m\n\x1b[47;30m 20 \x1b[0m\x1b[47;30m Jon \x1b[0m\x1b[47;30m Snow \x1b[0m\x1b[47;30m 2000 \x1b[0m\x1b[47;30m You know nothing, Jon Snow! \x1b[0m\n\x1b[107;30m 300 \x1b[0m\x1b[107;30m Tyrion \x1b[0m\x1b[107;30m Lannister \x1b[0m\x1b[107;30m 5000 \x1b[0m\x1b[107;30m \x1b[0m\n\x1b[41;30m \x1b[0m\x1b[41;30m \x1b[0m\x1b[41;30m TOTAL \x1b[0m\x1b[41;30m 10000 \x1b[0m\x1b[41;30m \x1b[0m", &StyleColoredBlackOnYellowWhite: "\x1b[103;30m # \x1b[0m\x1b[103;30m FIRST NAME \x1b[0m\x1b[103;30m LAST NAME \x1b[0m\x1b[103;30m SALARY \x1b[0m\x1b[103;30m \x1b[0m\n\x1b[107;30m 1 \x1b[0m\x1b[107;30m Arya \x1b[0m\x1b[107;30m Stark \x1b[0m\x1b[107;30m 3000 \x1b[0m\x1b[107;30m \x1b[0m\n\x1b[47;30m 20 \x1b[0m\x1b[47;30m Jon \x1b[0m\x1b[47;30m Snow \x1b[0m\x1b[47;30m 2000 \x1b[0m\x1b[47;30m You know nothing, Jon Snow! \x1b[0m\n\x1b[107;30m 300 \x1b[0m\x1b[107;30m Tyrion \x1b[0m\x1b[107;30m Lannister \x1b[0m\x1b[107;30m 5000 \x1b[0m\x1b[107;30m \x1b[0m\n\x1b[43;30m \x1b[0m\x1b[43;30m \x1b[0m\x1b[43;30m TOTAL \x1b[0m\x1b[43;30m 10000 \x1b[0m\x1b[43;30m \x1b[0m", &StyleColoredBlueWhiteOnBlack: "\x1b[94;100m # \x1b[0m\x1b[94;100m FIRST NAME \x1b[0m\x1b[94;100m LAST NAME \x1b[0m\x1b[94;100m SALARY \x1b[0m\x1b[94;100m \x1b[0m\n\x1b[97;40m 1 \x1b[0m\x1b[97;40m Arya \x1b[0m\x1b[97;40m Stark \x1b[0m\x1b[97;40m 3000 \x1b[0m\x1b[97;40m \x1b[0m\n\x1b[37;40m 20 \x1b[0m\x1b[37;40m Jon \x1b[0m\x1b[37;40m Snow \x1b[0m\x1b[37;40m 2000 \x1b[0m\x1b[37;40m You know nothing, Jon Snow! \x1b[0m\n\x1b[97;40m 300 \x1b[0m\x1b[97;40m Tyrion \x1b[0m\x1b[97;40m Lannister \x1b[0m\x1b[97;40m 5000 \x1b[0m\x1b[97;40m \x1b[0m\n\x1b[34;100m \x1b[0m\x1b[34;100m \x1b[0m\x1b[34;100m TOTAL \x1b[0m\x1b[34;100m 10000 \x1b[0m\x1b[34;100m \x1b[0m", &StyleColoredBright: "\x1b[106;30m # \x1b[0m\x1b[106;30m FIRST NAME \x1b[0m\x1b[106;30m LAST NAME \x1b[0m\x1b[106;30m SALARY \x1b[0m\x1b[106;30m \x1b[0m\n\x1b[107;30m 1 \x1b[0m\x1b[107;30m Arya \x1b[0m\x1b[107;30m Stark \x1b[0m\x1b[107;30m 3000 \x1b[0m\x1b[107;30m \x1b[0m\n\x1b[47;30m 20 \x1b[0m\x1b[47;30m Jon \x1b[0m\x1b[47;30m Snow \x1b[0m\x1b[47;30m 2000 \x1b[0m\x1b[47;30m You know nothing, Jon Snow! \x1b[0m\n\x1b[107;30m 300 \x1b[0m\x1b[107;30m Tyrion \x1b[0m\x1b[107;30m Lannister \x1b[0m\x1b[107;30m 5000 \x1b[0m\x1b[107;30m \x1b[0m\n\x1b[46;30m \x1b[0m\x1b[46;30m \x1b[0m\x1b[46;30m TOTAL \x1b[0m\x1b[46;30m 10000 \x1b[0m\x1b[46;30m \x1b[0m", &StyleColoredCyanWhiteOnBlack: "\x1b[96;100m # \x1b[0m\x1b[96;100m FIRST NAME \x1b[0m\x1b[96;100m LAST NAME \x1b[0m\x1b[96;100m SALARY \x1b[0m\x1b[96;100m \x1b[0m\n\x1b[97;40m 1 \x1b[0m\x1b[97;40m Arya \x1b[0m\x1b[97;40m Stark \x1b[0m\x1b[97;40m 3000 \x1b[0m\x1b[97;40m \x1b[0m\n\x1b[37;40m 20 \x1b[0m\x1b[37;40m Jon \x1b[0m\x1b[37;40m Snow \x1b[0m\x1b[37;40m 2000 \x1b[0m\x1b[37;40m You know nothing, Jon Snow! \x1b[0m\n\x1b[97;40m 300 \x1b[0m\x1b[97;40m Tyrion \x1b[0m\x1b[97;40m Lannister \x1b[0m\x1b[97;40m 5000 \x1b[0m\x1b[97;40m \x1b[0m\n\x1b[36;100m \x1b[0m\x1b[36;100m \x1b[0m\x1b[36;100m TOTAL \x1b[0m\x1b[36;100m 10000 \x1b[0m\x1b[36;100m \x1b[0m", &StyleColoredDark: "\x1b[96;100m # \x1b[0m\x1b[96;100m FIRST NAME \x1b[0m\x1b[96;100m LAST NAME \x1b[0m\x1b[96;100m SALARY \x1b[0m\x1b[96;100m \x1b[0m\n\x1b[97;40m 1 \x1b[0m\x1b[97;40m Arya \x1b[0m\x1b[97;40m Stark \x1b[0m\x1b[97;40m 3000 \x1b[0m\x1b[97;40m \x1b[0m\n\x1b[37;40m 20 \x1b[0m\x1b[37;40m Jon \x1b[0m\x1b[37;40m Snow \x1b[0m\x1b[37;40m 2000 \x1b[0m\x1b[37;40m You know nothing, Jon Snow! \x1b[0m\n\x1b[97;40m 300 \x1b[0m\x1b[97;40m Tyrion \x1b[0m\x1b[97;40m Lannister \x1b[0m\x1b[97;40m 5000 \x1b[0m\x1b[97;40m \x1b[0m\n\x1b[36;100m \x1b[0m\x1b[36;100m \x1b[0m\x1b[36;100m TOTAL \x1b[0m\x1b[36;100m 10000 \x1b[0m\x1b[36;100m \x1b[0m", &StyleColoredGreenWhiteOnBlack: "\x1b[92;100m # \x1b[0m\x1b[92;100m FIRST NAME \x1b[0m\x1b[92;100m LAST NAME \x1b[0m\x1b[92;100m SALARY \x1b[0m\x1b[92;100m \x1b[0m\n\x1b[97;40m 1 \x1b[0m\x1b[97;40m Arya \x1b[0m\x1b[97;40m Stark \x1b[0m\x1b[97;40m 3000 \x1b[0m\x1b[97;40m \x1b[0m\n\x1b[37;40m 20 \x1b[0m\x1b[37;40m Jon \x1b[0m\x1b[37;40m Snow \x1b[0m\x1b[37;40m 2000 \x1b[0m\x1b[37;40m You know nothing, Jon Snow! \x1b[0m\n\x1b[97;40m 300 \x1b[0m\x1b[97;40m Tyrion \x1b[0m\x1b[97;40m Lannister \x1b[0m\x1b[97;40m 5000 \x1b[0m\x1b[97;40m \x1b[0m\n\x1b[32;100m \x1b[0m\x1b[32;100m \x1b[0m\x1b[32;100m TOTAL \x1b[0m\x1b[32;100m 10000 \x1b[0m\x1b[32;100m \x1b[0m", &StyleColoredMagentaWhiteOnBlack: "\x1b[95;100m # \x1b[0m\x1b[95;100m FIRST NAME \x1b[0m\x1b[95;100m LAST NAME \x1b[0m\x1b[95;100m SALARY \x1b[0m\x1b[95;100m \x1b[0m\n\x1b[97;40m 1 \x1b[0m\x1b[97;40m Arya \x1b[0m\x1b[97;40m Stark \x1b[0m\x1b[97;40m 3000 \x1b[0m\x1b[97;40m \x1b[0m\n\x1b[37;40m 20 \x1b[0m\x1b[37;40m Jon \x1b[0m\x1b[37;40m Snow \x1b[0m\x1b[37;40m 2000 \x1b[0m\x1b[37;40m You know nothing, Jon Snow! \x1b[0m\n\x1b[97;40m 300 \x1b[0m\x1b[97;40m Tyrion \x1b[0m\x1b[97;40m Lannister \x1b[0m\x1b[97;40m 5000 \x1b[0m\x1b[97;40m \x1b[0m\n\x1b[35;100m \x1b[0m\x1b[35;100m \x1b[0m\x1b[35;100m TOTAL \x1b[0m\x1b[35;100m 10000 \x1b[0m\x1b[35;100m \x1b[0m", &StyleColoredRedWhiteOnBlack: "\x1b[91;100m # \x1b[0m\x1b[91;100m FIRST NAME \x1b[0m\x1b[91;100m LAST NAME \x1b[0m\x1b[91;100m SALARY \x1b[0m\x1b[91;100m \x1b[0m\n\x1b[97;40m 1 \x1b[0m\x1b[97;40m Arya \x1b[0m\x1b[97;40m Stark \x1b[0m\x1b[97;40m 3000 \x1b[0m\x1b[97;40m \x1b[0m\n\x1b[37;40m 20 \x1b[0m\x1b[37;40m Jon \x1b[0m\x1b[37;40m Snow \x1b[0m\x1b[37;40m 2000 \x1b[0m\x1b[37;40m You know nothing, Jon Snow! \x1b[0m\n\x1b[97;40m 300 \x1b[0m\x1b[97;40m Tyrion \x1b[0m\x1b[97;40m Lannister \x1b[0m\x1b[97;40m 5000 \x1b[0m\x1b[97;40m \x1b[0m\n\x1b[31;100m \x1b[0m\x1b[31;100m \x1b[0m\x1b[31;100m TOTAL \x1b[0m\x1b[31;100m 10000 \x1b[0m\x1b[31;100m \x1b[0m", &StyleColoredYellowWhiteOnBlack: "\x1b[93;100m # \x1b[0m\x1b[93;100m FIRST NAME \x1b[0m\x1b[93;100m LAST NAME \x1b[0m\x1b[93;100m SALARY \x1b[0m\x1b[93;100m \x1b[0m\n\x1b[97;40m 1 \x1b[0m\x1b[97;40m Arya \x1b[0m\x1b[97;40m Stark \x1b[0m\x1b[97;40m 3000 \x1b[0m\x1b[97;40m \x1b[0m\n\x1b[37;40m 20 \x1b[0m\x1b[37;40m Jon \x1b[0m\x1b[37;40m Snow \x1b[0m\x1b[37;40m 2000 \x1b[0m\x1b[37;40m You know nothing, Jon Snow! \x1b[0m\n\x1b[97;40m 300 \x1b[0m\x1b[97;40m Tyrion \x1b[0m\x1b[97;40m Lannister \x1b[0m\x1b[97;40m 5000 \x1b[0m\x1b[97;40m \x1b[0m\n\x1b[33;100m \x1b[0m\x1b[33;100m \x1b[0m\x1b[33;100m TOTAL \x1b[0m\x1b[33;100m 10000 \x1b[0m\x1b[33;100m \x1b[0m", &StyleDouble: "╔═════╦════════════╦═══════════╦════════╦═════════════════════════════╗\n║ # ║ FIRST NAME ║ LAST NAME ║ SALARY ║ ║\n╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣\n║ 1 ║ Arya ║ Stark ║ 3000 ║ ║\n║ 20 ║ Jon ║ Snow ║ 2000 ║ You know nothing, Jon Snow! ║\n║ 300 ║ Tyrion ║ Lannister ║ 5000 ║ ║\n╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣\n║ ║ ║ TOTAL ║ 10000 ║ ║\n╚═════╩════════════╩═══════════╩════════╩═════════════════════════════╝", &StyleLight: "┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐\n│ # │ FIRST NAME │ LAST NAME │ SALARY │ │\n├─────┼────────────┼───────────┼────────┼─────────────────────────────┤\n│ 1 │ Arya │ Stark │ 3000 │ │\n│ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │\n│ 300 │ Tyrion │ Lannister │ 5000 │ │\n├─────┼────────────┼───────────┼────────┼─────────────────────────────┤\n│ │ │ TOTAL │ 10000 │ │\n└─────┴────────────┴───────────┴────────┴─────────────────────────────┘", &StyleRounded: "╭─────┬────────────┬───────────┬────────┬─────────────────────────────╮\n│ # │ FIRST NAME │ LAST NAME │ SALARY │ │\n├─────┼────────────┼───────────┼────────┼─────────────────────────────┤\n│ 1 │ Arya │ Stark │ 3000 │ │\n│ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │\n│ 300 │ Tyrion │ Lannister │ 5000 │ │\n├─────┼────────────┼───────────┼────────┼─────────────────────────────┤\n│ │ │ TOTAL │ 10000 │ │\n╰─────┴────────────┴───────────┴────────┴─────────────────────────────╯", &styleTest: "(-----^------------^-----------^--------^-----------------------------)\n[< #>||||< >]\n{-----+------------+-----------+--------+-----------------------------}\n[< 1>|||< 3000>|< >]\n[< 20>|||< 2000>|]\n[<300>|||< 5000>|< >]\n{-----+------------+-----------+--------+-----------------------------}\n[< >|< >||< 10000>|< >]\n\\-----v------------v-----------v--------v-----------------------------/", } var mismatches []string for style, expectedOut := range styles { tw.SetStyle(*style) out := tw.Render() assert.Equal(t, expectedOut, out) if expectedOut != out { mismatches = append(mismatches, fmt.Sprintf("&%s: %#v,", style.Name, out)) fmt.Printf("// %s renders a Table like below:\n", style.Name) for _, line := range strings.Split(out, "\n") { fmt.Printf("// %s\n", line) } fmt.Println() } } sort.Strings(mismatches) for _, mismatch := range mismatches { fmt.Println(mismatch) } } func TestTable_Render_SuppressEmptyColumns(t *testing.T) { tw := NewWriter() tw.AppendHeader(testHeader) tw.AppendRows([]Row{ {1, "Arya", "", 3000}, {20, "Jon", "", 2000, "You know nothing, Jon Snow!"}, {300, "Tyrion", "", 5000}, }) tw.AppendRow(Row{11, "Sansa", "", 6000}) tw.AppendFooter(Row{"", "", "TOTAL", 10000}) tw.SetStyle(StyleLight) expectedOut := `┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐ │ # │ FIRST NAME │ LAST NAME │ SALARY │ │ ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ │ 1 │ Arya │ │ 3000 │ │ │ 20 │ Jon │ │ 2000 │ You know nothing, Jon Snow! │ │ 300 │ Tyrion │ │ 5000 │ │ │ 11 │ Sansa │ │ 6000 │ │ ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ │ │ │ TOTAL │ 10000 │ │ └─────┴────────────┴───────────┴────────┴─────────────────────────────┘` assert.Equal(t, expectedOut, tw.Render()) tw.SuppressEmptyColumns() expectedOut = `┌─────┬────────────┬────────┬─────────────────────────────┐ │ # │ FIRST NAME │ SALARY │ │ ├─────┼────────────┼────────┼─────────────────────────────┤ │ 1 │ Arya │ 3000 │ │ │ 20 │ Jon │ 2000 │ You know nothing, Jon Snow! │ │ 300 │ Tyrion │ 5000 │ │ │ 11 │ Sansa │ 6000 │ │ ├─────┼────────────┼────────┼─────────────────────────────┤ │ │ │ 10000 │ │ └─────┴────────────┴────────┴─────────────────────────────┘` assert.Equal(t, expectedOut, tw.Render()) } func TestTable_Render_TableWithinTable(t *testing.T) { twInner := NewWriter() twInner.AppendHeader(testHeader) twInner.AppendRows(testRows) twInner.AppendFooter(testFooter) twInner.SetStyle(StyleLight) twOuter := NewWriter() twOuter.AppendHeader(Row{"Table within a Table"}) twOuter.AppendRow(Row{twInner.Render()}) twOuter.SetColumnConfigs([]ColumnConfig{{Number: 1, AlignHeader: text.AlignCenter}}) twOuter.SetStyle(StyleDouble) expectedOut := `╔═════════════════════════════════════════════════════════════════════════╗ ║ TABLE WITHIN A TABLE ║ ╠═════════════════════════════════════════════════════════════════════════╣ ║ ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐ ║ ║ │ # │ FIRST NAME │ LAST NAME │ SALARY │ │ ║ ║ ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ ║ ║ │ 1 │ Arya │ Stark │ 3000 │ │ ║ ║ │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ ║ ║ │ 300 │ Tyrion │ Lannister │ 5000 │ │ ║ ║ ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ ║ ║ │ │ │ TOTAL │ 10000 │ │ ║ ║ └─────┴────────────┴───────────┴────────┴─────────────────────────────┘ ║ ╚═════════════════════════════════════════════════════════════════════════╝` assert.Equal(t, expectedOut, twOuter.Render()) } func TestTable_Render_TableWithTransformers(t *testing.T) { bolden := func(val interface{}) string { return text.Bold.Sprint(val) } tw := NewWriter() tw.AppendHeader(testHeader) tw.AppendRows(testRows) tw.AppendFooter(testFooter) tw.SetColumnConfigs([]ColumnConfig{{ Name: "Salary", Transformer: bolden, TransformerFooter: bolden, TransformerHeader: bolden, }}) tw.SetStyle(StyleLight) expectedOut := []string{ "┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐", "│ # │ FIRST NAME │ LAST NAME │ \x1b[1mSALARY\x1b[0m │ │", "├─────┼────────────┼───────────┼────────┼─────────────────────────────┤", "│ 1 │ Arya │ Stark │ \x1b[1m3000\x1b[0m │ │", "│ 20 │ Jon │ Snow │ \x1b[1m2000\x1b[0m │ You know nothing, Jon Snow! │", "│ 300 │ Tyrion │ Lannister │ \x1b[1m5000\x1b[0m │ │", "├─────┼────────────┼───────────┼────────┼─────────────────────────────┤", "│ │ │ TOTAL │ \x1b[1m10000\x1b[0m │ │", "└─────┴────────────┴───────────┴────────┴─────────────────────────────┘", } out := tw.Render() assert.Equal(t, strings.Join(expectedOut, "\n"), out) if strings.Join(expectedOut, "\n") != out { for _, line := range strings.Split(out, "\n") { fmt.Printf("%#v,\n", line) } } } func TestTable_Render_SetWidth_Title(t *testing.T) { tw := NewWriter() tw.AppendHeader(testHeader) tw.AppendRows(testRows) tw.AppendFooter(testFooter) tw.SetTitle("Game Of Thrones") t.Run("length 20", func(t *testing.T) { tw.SetAllowedRowLength(20) expectedOut := []string{ "+------------------+", "| Game Of Thrones |", "+-----+----------- ~", "| # | FIRST NAME ~", "+-----+----------- ~", "| 1 | Arya ~", "| 20 | Jon ~", "| 300 | Tyrion ~", "+-----+----------- ~", "| | ~", "+-----+----------- ~", } assert.Equal(t, strings.Join(expectedOut, "\n"), tw.Render()) }) t.Run("length 30", func(t *testing.T) { tw.SetAllowedRowLength(30) expectedOut := []string{ "+----------------------------+", "| Game Of Thrones |", "+-----+------------+-------- ~", "| # | FIRST NAME | LAST NA ~", "+-----+------------+-------- ~", "| 1 | Arya | Stark ~", "| 20 | Jon | Snow ~", "| 300 | Tyrion | Lannist ~", "+-----+------------+-------- ~", "| | | TOTAL ~", "+-----+------------+-------- ~", } assert.Equal(t, strings.Join(expectedOut, "\n"), tw.Render()) }) } func TestTable_Render_WidthEnforcer(t *testing.T) { tw := NewWriter() tw.AppendRows([]Row{ {"U2", "Hey", "2021-04-19 13:37", "Yuh yuh yuh"}, {"S12", "Uhhhh", "2021-04-19 13:37", "Some dummy data here"}, {"R123", "Lobsters", "2021-04-19 13:37", "I like lobsters"}, {"R123", "Some big name here and it's pretty big", "2021-04-19 13:37", "Abcdefghijklmnopqrstuvwxyz"}, {"R123", "Small name", "2021-04-19 13:37", "Abcdefghijklmnopqrstuvwxyz"}, }) tw.SetColumnConfigs([]ColumnConfig{ {Number: 2, WidthMax: 20, WidthMaxEnforcer: text.Trim}, }) expectedOut := `+------+----------------------+------------------+----------------------------+ | U2 | Hey | 2021-04-19 13:37 | Yuh yuh yuh | | S12 | Uhhhh | 2021-04-19 13:37 | Some dummy data here | | R123 | Lobsters | 2021-04-19 13:37 | I like lobsters | | R123 | Some big name here a | 2021-04-19 13:37 | Abcdefghijklmnopqrstuvwxyz | | R123 | Small name | 2021-04-19 13:37 | Abcdefghijklmnopqrstuvwxyz | +------+----------------------+------------------+----------------------------+` actualOut := tw.Render() assert.Equal(t, expectedOut, actualOut) if expectedOut != actualOut { fmt.Println(actualOut) } } go-pretty-6.2.4/table/sort.go000066400000000000000000000056461407250454200161000ustar00rootroot00000000000000package table import ( "sort" "strconv" ) // SortBy defines What to sort (Column Name or Number), and How to sort (Mode). type SortBy struct { // Name is the name of the Column as it appears in the first Header row. // If a Header is not provided, or the name is not found in the header, this // will not work. Name string // Number is the Column # from left. When specified, it overrides the Name // property. If you know the exact Column number, use this instead of Name. Number int // Mode tells the Writer how to Sort. Asc/Dsc/etc. Mode SortMode } // SortMode defines How to sort. type SortMode int const ( // Asc sorts the column in Ascending order alphabetically. Asc SortMode = iota // AscNumeric sorts the column in Ascending order numerically. AscNumeric // Dsc sorts the column in Descending order alphabetically. Dsc // DscNumeric sorts the column in Descending order numerically. DscNumeric ) type rowsSorter struct { rows []rowStr sortBy []SortBy sortedIndices []int } // getSortedRowIndices sorts and returns the row indices in Sorted order as // directed by Table.sortBy which can be set using Table.SortBy(...) func (t *Table) getSortedRowIndices() []int { sortedIndices := make([]int, len(t.rows)) for idx := range t.rows { sortedIndices[idx] = idx } if t.sortBy != nil && len(t.sortBy) > 0 { sort.Sort(rowsSorter{ rows: t.rows, sortBy: t.parseSortBy(t.sortBy), sortedIndices: sortedIndices, }) } return sortedIndices } func (t *Table) parseSortBy(sortBy []SortBy) []SortBy { var resSortBy []SortBy for _, col := range sortBy { colNum := 0 if col.Number > 0 && col.Number <= t.numColumns { colNum = col.Number } else if col.Name != "" && len(t.rowsHeader) > 0 { for idx, colName := range t.rowsHeader[0] { if col.Name == colName { colNum = idx + 1 break } } } if colNum > 0 { resSortBy = append(resSortBy, SortBy{ Name: col.Name, Number: colNum, Mode: col.Mode, }) } } return resSortBy } func (rs rowsSorter) Len() int { return len(rs.rows) } func (rs rowsSorter) Swap(i, j int) { rs.sortedIndices[i], rs.sortedIndices[j] = rs.sortedIndices[j], rs.sortedIndices[i] } func (rs rowsSorter) Less(i, j int) bool { realI, realJ := rs.sortedIndices[i], rs.sortedIndices[j] for _, col := range rs.sortBy { rowI, rowJ, colIdx := rs.rows[realI], rs.rows[realJ], col.Number-1 if colIdx < len(rowI) && colIdx < len(rowJ) { if rowI[colIdx] == rowJ[colIdx] { continue } else if col.Mode == Asc { return rowI[colIdx] < rowJ[colIdx] } else if col.Mode == Dsc { return rowI[colIdx] > rowJ[colIdx] } iVal, iErr := strconv.ParseFloat(rowI[colIdx], 64) jVal, jErr := strconv.ParseFloat(rowJ[colIdx], 64) if iErr == nil && jErr == nil { if col.Mode == AscNumeric { return iVal < jVal } else if col.Mode == DscNumeric { return jVal < iVal } } } } return false } go-pretty-6.2.4/table/sort_test.go000066400000000000000000000115041407250454200171250ustar00rootroot00000000000000package table import ( "github.com/stretchr/testify/assert" "testing" ) func TestTable_sortRows_WithName(t *testing.T) { table := Table{} table.AppendHeader(Row{"#", "First Name", "Last Name", "Salary"}) table.AppendRows([]Row{ {1, "Arya", "Stark", 3000}, {11, "Sansa", "Stark", 3000}, {20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"}, {300, "Tyrion", "Lannister", 5000}, }) table.initForRenderRows() // sort by nothing assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices()) // sort by "#" table.SortBy([]SortBy{{Name: "#", Mode: AscNumeric}}) assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices()) table.SortBy([]SortBy{{Name: "#", Mode: DscNumeric}}) assert.Equal(t, []int{3, 2, 1, 0}, table.getSortedRowIndices()) // sort by First Name, Last Name table.SortBy([]SortBy{{Name: "First Name", Mode: Asc}, {Name: "Last Name", Mode: Asc}}) assert.Equal(t, []int{0, 2, 1, 3}, table.getSortedRowIndices()) table.SortBy([]SortBy{{Name: "First Name", Mode: Asc}, {Name: "Last Name", Mode: Dsc}}) assert.Equal(t, []int{0, 2, 1, 3}, table.getSortedRowIndices()) table.SortBy([]SortBy{{Name: "First Name", Mode: Dsc}, {Name: "Last Name", Mode: Asc}}) assert.Equal(t, []int{3, 1, 2, 0}, table.getSortedRowIndices()) table.SortBy([]SortBy{{Name: "First Name", Mode: Dsc}, {Name: "Last Name", Mode: Dsc}}) assert.Equal(t, []int{3, 1, 2, 0}, table.getSortedRowIndices()) // sort by Last Name, First Name table.SortBy([]SortBy{{Name: "Last Name", Mode: Asc}, {Name: "First Name", Mode: Asc}}) assert.Equal(t, []int{3, 2, 0, 1}, table.getSortedRowIndices()) table.SortBy([]SortBy{{Name: "Last Name", Mode: Asc}, {Name: "First Name", Mode: Dsc}}) assert.Equal(t, []int{3, 2, 1, 0}, table.getSortedRowIndices()) table.SortBy([]SortBy{{Name: "Last Name", Mode: Dsc}, {Name: "First Name", Mode: Asc}}) assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices()) table.SortBy([]SortBy{{Name: "Last Name", Mode: Dsc}, {Name: "First Name", Mode: Dsc}}) assert.Equal(t, []int{1, 0, 2, 3}, table.getSortedRowIndices()) // sort by Unknown Column table.SortBy([]SortBy{{Name: "Last Name", Mode: Dsc}, {Name: "Foo Bar", Mode: Dsc}}) assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices()) // sort by Salary table.SortBy([]SortBy{{Name: "Salary", Mode: AscNumeric}}) assert.Equal(t, []int{2, 0, 1, 3}, table.getSortedRowIndices()) table.SortBy([]SortBy{{Name: "Salary", Mode: DscNumeric}}) assert.Equal(t, []int{3, 0, 1, 2}, table.getSortedRowIndices()) table.SortBy(nil) assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices()) } func TestTable_sortRows_WithoutName(t *testing.T) { table := Table{} table.AppendRows([]Row{ {1, "Arya", "Stark", 3000}, {11, "Sansa", "Stark", 3000}, {20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"}, {300, "Tyrion", "Lannister", 5000}, }) table.initForRenderRows() // sort by nothing assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices()) // sort by "#" table.SortBy([]SortBy{{Number: 1, Mode: AscNumeric}}) assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices()) table.SortBy([]SortBy{{Number: 1, Mode: DscNumeric}}) assert.Equal(t, []int{3, 2, 1, 0}, table.getSortedRowIndices()) // sort by First Name, Last Name table.SortBy([]SortBy{{Number: 2, Mode: Asc}, {Number: 3, Mode: Asc}}) assert.Equal(t, []int{0, 2, 1, 3}, table.getSortedRowIndices()) table.SortBy([]SortBy{{Number: 2, Mode: Asc}, {Number: 3, Mode: Dsc}}) assert.Equal(t, []int{0, 2, 1, 3}, table.getSortedRowIndices()) table.SortBy([]SortBy{{Number: 2, Mode: Dsc}, {Number: 3, Mode: Asc}}) assert.Equal(t, []int{3, 1, 2, 0}, table.getSortedRowIndices()) table.SortBy([]SortBy{{Number: 2, Mode: Dsc}, {Number: 3, Mode: Dsc}}) assert.Equal(t, []int{3, 1, 2, 0}, table.getSortedRowIndices()) // sort by Last Name, First Name table.SortBy([]SortBy{{Number: 3, Mode: Asc}, {Number: 2, Mode: Asc}}) assert.Equal(t, []int{3, 2, 0, 1}, table.getSortedRowIndices()) table.SortBy([]SortBy{{Number: 3, Mode: Asc}, {Number: 2, Mode: Dsc}}) assert.Equal(t, []int{3, 2, 1, 0}, table.getSortedRowIndices()) table.SortBy([]SortBy{{Number: 3, Mode: Dsc}, {Number: 2, Mode: Asc}}) assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices()) table.SortBy([]SortBy{{Number: 3, Mode: Dsc}, {Number: 2, Mode: Dsc}}) assert.Equal(t, []int{1, 0, 2, 3}, table.getSortedRowIndices()) // sort by Unknown Column table.SortBy([]SortBy{{Number: 3, Mode: Dsc}, {Number: 99, Mode: Dsc}}) assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices()) // sort by Salary table.SortBy([]SortBy{{Number: 4, Mode: AscNumeric}}) assert.Equal(t, []int{2, 0, 1, 3}, table.getSortedRowIndices()) table.SortBy([]SortBy{{Number: 4, Mode: DscNumeric}}) assert.Equal(t, []int{3, 0, 1, 2}, table.getSortedRowIndices()) table.SortBy(nil) assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices()) } go-pretty-6.2.4/table/style.go000066400000000000000000001245561407250454200162530ustar00rootroot00000000000000package table import ( "github.com/jedib0t/go-pretty/v6/text" ) // Style declares how to render the Table and provides very fine-grained control // on how the Table gets rendered on the Console. type Style struct { Name string // name of the Style Box BoxStyle // characters to use for the boxes Color ColorOptions // colors to use for the rows and columns Format FormatOptions // formatting options for the rows and columns HTML HTMLOptions // rendering options for HTML mode Options Options // misc. options for the table Title TitleOptions // formation options for the title text } var ( // StyleDefault renders a Table like below: // +-----+------------+-----------+--------+-----------------------------+ // | # | FIRST NAME | LAST NAME | SALARY | | // +-----+------------+-----------+--------+-----------------------------+ // | 1 | Arya | Stark | 3000 | | // | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | // | 300 | Tyrion | Lannister | 5000 | | // +-----+------------+-----------+--------+-----------------------------+ // | | | TOTAL | 10000 | | // +-----+------------+-----------+--------+-----------------------------+ StyleDefault = Style{ Name: "StyleDefault", Box: StyleBoxDefault, Color: ColorOptionsDefault, Format: FormatOptionsDefault, HTML: DefaultHTMLOptions, Options: OptionsDefault, Title: TitleOptionsDefault, } // StyleBold renders a Table like below: // ┏━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ // ┃ # ┃ FIRST NAME ┃ LAST NAME ┃ SALARY ┃ ┃ // ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ // ┃ 1 ┃ Arya ┃ Stark ┃ 3000 ┃ ┃ // ┃ 20 ┃ Jon ┃ Snow ┃ 2000 ┃ You know nothing, Jon Snow! ┃ // ┃ 300 ┃ Tyrion ┃ Lannister ┃ 5000 ┃ ┃ // ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ // ┃ ┃ ┃ TOTAL ┃ 10000 ┃ ┃ // ┗━━━━━┻━━━━━━━━━━━━┻━━━━━━━━━━━┻━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ StyleBold = Style{ Name: "StyleBold", Box: StyleBoxBold, Color: ColorOptionsDefault, Format: FormatOptionsDefault, HTML: DefaultHTMLOptions, Options: OptionsDefault, Title: TitleOptionsDefault, } // StyleColoredBright renders a Table without any borders or separators, // and with Black text on Cyan background for Header/Footer and // White background for other rows. StyleColoredBright = Style{ Name: "StyleColoredBright", Box: StyleBoxDefault, Color: ColorOptionsBright, Format: FormatOptionsDefault, HTML: DefaultHTMLOptions, Options: OptionsNoBordersAndSeparators, Title: TitleOptionsDark, } // StyleColoredDark renders a Table without any borders or separators, and // with Header/Footer in Cyan text and other rows with White text, all on // Black background. StyleColoredDark = Style{ Name: "StyleColoredDark", Box: StyleBoxDefault, Color: ColorOptionsDark, Format: FormatOptionsDefault, HTML: DefaultHTMLOptions, Options: OptionsNoBordersAndSeparators, Title: TitleOptionsBright, } // StyleColoredBlackOnBlueWhite renders a Table without any borders or // separators, and with Black text on Blue background for Header/Footer and // White background for other rows. StyleColoredBlackOnBlueWhite = Style{ Name: "StyleColoredBlackOnBlueWhite", Box: StyleBoxDefault, Color: ColorOptionsBlackOnBlueWhite, Format: FormatOptionsDefault, HTML: DefaultHTMLOptions, Options: OptionsNoBordersAndSeparators, Title: TitleOptionsBlueOnBlack, } // StyleColoredBlackOnCyanWhite renders a Table without any borders or // separators, and with Black text on Cyan background for Header/Footer and // White background for other rows. StyleColoredBlackOnCyanWhite = Style{ Name: "StyleColoredBlackOnCyanWhite", Box: StyleBoxDefault, Color: ColorOptionsBlackOnCyanWhite, Format: FormatOptionsDefault, HTML: DefaultHTMLOptions, Options: OptionsNoBordersAndSeparators, Title: TitleOptionsCyanOnBlack, } // StyleColoredBlackOnGreenWhite renders a Table without any borders or // separators, and with Black text on Green background for Header/Footer and // White background for other rows. StyleColoredBlackOnGreenWhite = Style{ Name: "StyleColoredBlackOnGreenWhite", Box: StyleBoxDefault, Color: ColorOptionsBlackOnGreenWhite, Format: FormatOptionsDefault, HTML: DefaultHTMLOptions, Options: OptionsNoBordersAndSeparators, Title: TitleOptionsGreenOnBlack, } // StyleColoredBlackOnMagentaWhite renders a Table without any borders or // separators, and with Black text on Magenta background for Header/Footer and // White background for other rows. StyleColoredBlackOnMagentaWhite = Style{ Name: "StyleColoredBlackOnMagentaWhite", Box: StyleBoxDefault, Color: ColorOptionsBlackOnMagentaWhite, Format: FormatOptionsDefault, HTML: DefaultHTMLOptions, Options: OptionsNoBordersAndSeparators, Title: TitleOptionsMagentaOnBlack, } // StyleColoredBlackOnYellowWhite renders a Table without any borders or // separators, and with Black text on Yellow background for Header/Footer and // White background for other rows. StyleColoredBlackOnYellowWhite = Style{ Name: "StyleColoredBlackOnYellowWhite", Box: StyleBoxDefault, Color: ColorOptionsBlackOnYellowWhite, Format: FormatOptionsDefault, HTML: DefaultHTMLOptions, Options: OptionsNoBordersAndSeparators, Title: TitleOptionsYellowOnBlack, } // StyleColoredBlackOnRedWhite renders a Table without any borders or // separators, and with Black text on Red background for Header/Footer and // White background for other rows. StyleColoredBlackOnRedWhite = Style{ Name: "StyleColoredBlackOnRedWhite", Box: StyleBoxDefault, Color: ColorOptionsBlackOnRedWhite, Format: FormatOptionsDefault, HTML: DefaultHTMLOptions, Options: OptionsNoBordersAndSeparators, Title: TitleOptionsRedOnBlack, } // StyleColoredBlueWhiteOnBlack renders a Table without any borders or // separators, and with Header/Footer in Blue text and other rows with // White text, all on Black background. StyleColoredBlueWhiteOnBlack = Style{ Name: "StyleColoredBlueWhiteOnBlack", Box: StyleBoxDefault, Color: ColorOptionsBlueWhiteOnBlack, Format: FormatOptionsDefault, HTML: DefaultHTMLOptions, Options: OptionsNoBordersAndSeparators, Title: TitleOptionsBlackOnBlue, } // StyleColoredCyanWhiteOnBlack renders a Table without any borders or // separators, and with Header/Footer in Cyan text and other rows with // White text, all on Black background. StyleColoredCyanWhiteOnBlack = Style{ Name: "StyleColoredCyanWhiteOnBlack", Box: StyleBoxDefault, Color: ColorOptionsCyanWhiteOnBlack, Format: FormatOptionsDefault, HTML: DefaultHTMLOptions, Options: OptionsNoBordersAndSeparators, Title: TitleOptionsBlackOnCyan, } // StyleColoredGreenWhiteOnBlack renders a Table without any borders or // separators, and with Header/Footer in Green text and other rows with // White text, all on Black background. StyleColoredGreenWhiteOnBlack = Style{ Name: "StyleColoredGreenWhiteOnBlack", Box: StyleBoxDefault, Color: ColorOptionsGreenWhiteOnBlack, Format: FormatOptionsDefault, HTML: DefaultHTMLOptions, Options: OptionsNoBordersAndSeparators, Title: TitleOptionsBlackOnGreen, } // StyleColoredMagentaWhiteOnBlack renders a Table without any borders or // separators, and with Header/Footer in Magenta text and other rows with // White text, all on Black background. StyleColoredMagentaWhiteOnBlack = Style{ Name: "StyleColoredMagentaWhiteOnBlack", Box: StyleBoxDefault, Color: ColorOptionsMagentaWhiteOnBlack, Format: FormatOptionsDefault, HTML: DefaultHTMLOptions, Options: OptionsNoBordersAndSeparators, Title: TitleOptionsBlackOnMagenta, } // StyleColoredRedWhiteOnBlack renders a Table without any borders or // separators, and with Header/Footer in Red text and other rows with // White text, all on Black background. StyleColoredRedWhiteOnBlack = Style{ Name: "StyleColoredRedWhiteOnBlack", Box: StyleBoxDefault, Color: ColorOptionsRedWhiteOnBlack, Format: FormatOptionsDefault, HTML: DefaultHTMLOptions, Options: OptionsNoBordersAndSeparators, Title: TitleOptionsBlackOnRed, } // StyleColoredYellowWhiteOnBlack renders a Table without any borders or // separators, and with Header/Footer in Yellow text and other rows with // White text, all on Black background. StyleColoredYellowWhiteOnBlack = Style{ Name: "StyleColoredYellowWhiteOnBlack", Box: StyleBoxDefault, Color: ColorOptionsYellowWhiteOnBlack, Format: FormatOptionsDefault, HTML: DefaultHTMLOptions, Options: OptionsNoBordersAndSeparators, Title: TitleOptionsBlackOnYellow, } // StyleDouble renders a Table like below: // ╔═════╦════════════╦═══════════╦════════╦═════════════════════════════╗ // ║ # ║ FIRST NAME ║ LAST NAME ║ SALARY ║ ║ // ╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣ // ║ 1 ║ Arya ║ Stark ║ 3000 ║ ║ // ║ 20 ║ Jon ║ Snow ║ 2000 ║ You know nothing, Jon Snow! ║ // ║ 300 ║ Tyrion ║ Lannister ║ 5000 ║ ║ // ╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣ // ║ ║ ║ TOTAL ║ 10000 ║ ║ // ╚═════╩════════════╩═══════════╩════════╩═════════════════════════════╝ StyleDouble = Style{ Name: "StyleDouble", Box: StyleBoxDouble, Color: ColorOptionsDefault, Format: FormatOptionsDefault, HTML: DefaultHTMLOptions, Options: OptionsDefault, Title: TitleOptionsDefault, } // StyleLight renders a Table like below: // ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐ // │ # │ FIRST NAME │ LAST NAME │ SALARY │ │ // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ // │ 1 │ Arya │ Stark │ 3000 │ │ // │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ // │ 300 │ Tyrion │ Lannister │ 5000 │ │ // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ // │ │ │ TOTAL │ 10000 │ │ // └─────┴────────────┴───────────┴────────┴─────────────────────────────┘ StyleLight = Style{ Name: "StyleLight", Box: StyleBoxLight, Color: ColorOptionsDefault, Format: FormatOptionsDefault, HTML: DefaultHTMLOptions, Options: OptionsDefault, Title: TitleOptionsDefault, } // StyleRounded renders a Table like below: // ╭─────┬────────────┬───────────┬────────┬─────────────────────────────╮ // │ # │ FIRST NAME │ LAST NAME │ SALARY │ │ // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ // │ 1 │ Arya │ Stark │ 3000 │ │ // │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ // │ 300 │ Tyrion │ Lannister │ 5000 │ │ // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ // │ │ │ TOTAL │ 10000 │ │ // ╰─────┴────────────┴───────────┴────────┴─────────────────────────────╯ StyleRounded = Style{ Name: "StyleRounded", Box: StyleBoxRounded, Color: ColorOptionsDefault, Format: FormatOptionsDefault, HTML: DefaultHTMLOptions, Options: OptionsDefault, Title: TitleOptionsDefault, } // styleTest renders a Table like below: // (-----^------------^-----------^--------^-----------------------------) // [< #>||||< >] // {-----+------------+-----------+--------+-----------------------------} // [< 1>|||< 3000>|< >] // [< 20>|||< 2000>|] // [<300>|||< 5000>|< >] // {-----+------------+-----------+--------+-----------------------------} // [< >|< >||< 10000>|< >] // \-----v------------v-----------v--------v-----------------------------/ styleTest = Style{ Name: "styleTest", Box: styleBoxTest, Color: ColorOptionsDefault, Format: FormatOptionsDefault, HTML: DefaultHTMLOptions, Options: OptionsDefault, Title: TitleOptionsDefault, } ) // BoxStyle defines the characters/strings to use to render the borders and // separators for the Table. type BoxStyle struct { BottomLeft string BottomRight string BottomSeparator string EmptySeparator string Left string LeftSeparator string MiddleHorizontal string MiddleSeparator string MiddleVertical string PaddingLeft string PaddingRight string PageSeparator string Right string RightSeparator string TopLeft string TopRight string TopSeparator string UnfinishedRow string } var ( // StyleBoxDefault defines a Boxed-Table like below: // +-----+------------+-----------+--------+-----------------------------+ // | # | FIRST NAME | LAST NAME | SALARY | | // +-----+------------+-----------+--------+-----------------------------+ // | 1 | Arya | Stark | 3000 | | // | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | // | 300 | Tyrion | Lannister | 5000 | | // +-----+------------+-----------+--------+-----------------------------+ // | | | TOTAL | 10000 | | // +-----+------------+-----------+--------+-----------------------------+ StyleBoxDefault = BoxStyle{ BottomLeft: "+", BottomRight: "+", BottomSeparator: "+", EmptySeparator: text.RepeatAndTrim(" ", text.RuneCount("+")), Left: "|", LeftSeparator: "+", MiddleHorizontal: "-", MiddleSeparator: "+", MiddleVertical: "|", PaddingLeft: " ", PaddingRight: " ", PageSeparator: "\n", Right: "|", RightSeparator: "+", TopLeft: "+", TopRight: "+", TopSeparator: "+", UnfinishedRow: " ~", } // StyleBoxBold defines a Boxed-Table like below: // ┏━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ // ┃ # ┃ FIRST NAME ┃ LAST NAME ┃ SALARY ┃ ┃ // ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ // ┃ 1 ┃ Arya ┃ Stark ┃ 3000 ┃ ┃ // ┃ 20 ┃ Jon ┃ Snow ┃ 2000 ┃ You know nothing, Jon Snow! ┃ // ┃ 300 ┃ Tyrion ┃ Lannister ┃ 5000 ┃ ┃ // ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ // ┃ ┃ ┃ TOTAL ┃ 10000 ┃ ┃ // ┗━━━━━┻━━━━━━━━━━━━┻━━━━━━━━━━━┻━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ StyleBoxBold = BoxStyle{ BottomLeft: "┗", BottomRight: "┛", BottomSeparator: "┻", EmptySeparator: text.RepeatAndTrim(" ", text.RuneCount("╋")), Left: "┃", LeftSeparator: "┣", MiddleHorizontal: "━", MiddleSeparator: "╋", MiddleVertical: "┃", PaddingLeft: " ", PaddingRight: " ", PageSeparator: "\n", Right: "┃", RightSeparator: "┫", TopLeft: "┏", TopRight: "┓", TopSeparator: "┳", UnfinishedRow: " ≈", } // StyleBoxDouble defines a Boxed-Table like below: // ╔═════╦════════════╦═══════════╦════════╦═════════════════════════════╗ // ║ # ║ FIRST NAME ║ LAST NAME ║ SALARY ║ ║ // ╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣ // ║ 1 ║ Arya ║ Stark ║ 3000 ║ ║ // ║ 20 ║ Jon ║ Snow ║ 2000 ║ You know nothing, Jon Snow! ║ // ║ 300 ║ Tyrion ║ Lannister ║ 5000 ║ ║ // ╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣ // ║ ║ ║ TOTAL ║ 10000 ║ ║ // ╚═════╩════════════╩═══════════╩════════╩═════════════════════════════╝ StyleBoxDouble = BoxStyle{ BottomLeft: "╚", BottomRight: "╝", BottomSeparator: "╩", EmptySeparator: text.RepeatAndTrim(" ", text.RuneCount("╬")), Left: "║", LeftSeparator: "╠", MiddleHorizontal: "═", MiddleSeparator: "╬", MiddleVertical: "║", PaddingLeft: " ", PaddingRight: " ", PageSeparator: "\n", Right: "║", RightSeparator: "╣", TopLeft: "╔", TopRight: "╗", TopSeparator: "╦", UnfinishedRow: " ≈", } // StyleBoxLight defines a Boxed-Table like below: // ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐ // │ # │ FIRST NAME │ LAST NAME │ SALARY │ │ // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ // │ 1 │ Arya │ Stark │ 3000 │ │ // │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ // │ 300 │ Tyrion │ Lannister │ 5000 │ │ // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ // │ │ │ TOTAL │ 10000 │ │ // └─────┴────────────┴───────────┴────────┴─────────────────────────────┘ StyleBoxLight = BoxStyle{ BottomLeft: "└", BottomRight: "┘", BottomSeparator: "┴", EmptySeparator: text.RepeatAndTrim(" ", text.RuneCount("┼")), Left: "│", LeftSeparator: "├", MiddleHorizontal: "─", MiddleSeparator: "┼", MiddleVertical: "│", PaddingLeft: " ", PaddingRight: " ", PageSeparator: "\n", Right: "│", RightSeparator: "┤", TopLeft: "┌", TopRight: "┐", TopSeparator: "┬", UnfinishedRow: " ≈", } // StyleBoxRounded defines a Boxed-Table like below: // ╭─────┬────────────┬───────────┬────────┬─────────────────────────────╮ // │ # │ FIRST NAME │ LAST NAME │ SALARY │ │ // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ // │ 1 │ Arya │ Stark │ 3000 │ │ // │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ // │ 300 │ Tyrion │ Lannister │ 5000 │ │ // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ // │ │ │ TOTAL │ 10000 │ │ // ╰─────┴────────────┴───────────┴────────┴─────────────────────────────╯ StyleBoxRounded = BoxStyle{ BottomLeft: "╰", BottomRight: "╯", BottomSeparator: "┴", EmptySeparator: text.RepeatAndTrim(" ", text.RuneCount("┼")), Left: "│", LeftSeparator: "├", MiddleHorizontal: "─", MiddleSeparator: "┼", MiddleVertical: "│", PaddingLeft: " ", PaddingRight: " ", PageSeparator: "\n", Right: "│", RightSeparator: "┤", TopLeft: "╭", TopRight: "╮", TopSeparator: "┬", UnfinishedRow: " ≈", } // styleBoxTest defines a Boxed-Table like below: // (-----^------------^-----------^--------^-----------------------------) // [< #>||||< >] // {-----+------------+-----------+--------+-----------------------------} // [< 1>|||< 3000>|< >] // [< 20>|||< 2000>|] // [<300>|||< 5000>|< >] // {-----+------------+-----------+--------+-----------------------------} // [< >|< >||< 10000>|< >] // \-----v------------v-----------v--------v-----------------------------/ styleBoxTest = BoxStyle{ BottomLeft: "\\", BottomRight: "/", BottomSeparator: "v", EmptySeparator: text.RepeatAndTrim(" ", text.RuneCount("+")), Left: "[", LeftSeparator: "{", MiddleHorizontal: "--", MiddleSeparator: "+", MiddleVertical: "|", PaddingLeft: "<", PaddingRight: ">", PageSeparator: "\n", Right: "]", RightSeparator: "}", TopLeft: "(", TopRight: ")", TopSeparator: "^", UnfinishedRow: " ~~~", } ) // ColorOptions defines the ANSI colors to use for parts of the Table. type ColorOptions struct { IndexColumn text.Colors // index-column colors (row #, etc.) Footer text.Colors // footer row(s) colors Header text.Colors // header row(s) colors Row text.Colors // regular row(s) colors RowAlternate text.Colors // regular row(s) colors for the even-numbered rows } var ( // ColorOptionsDefault defines sensible ANSI color options - basically NONE. ColorOptionsDefault = ColorOptions{} // ColorOptionsBright renders dark text on bright background. ColorOptionsBright = ColorOptionsBlackOnCyanWhite // ColorOptionsDark renders bright text on dark background. ColorOptionsDark = ColorOptionsCyanWhiteOnBlack // ColorOptionsBlackOnBlueWhite renders Black text on Blue/White background. ColorOptionsBlackOnBlueWhite = ColorOptions{ IndexColumn: text.Colors{text.BgHiBlue, text.FgBlack}, Footer: text.Colors{text.BgBlue, text.FgBlack}, Header: text.Colors{text.BgHiBlue, text.FgBlack}, Row: text.Colors{text.BgHiWhite, text.FgBlack}, RowAlternate: text.Colors{text.BgWhite, text.FgBlack}, } // ColorOptionsBlackOnCyanWhite renders Black text on Cyan/White background. ColorOptionsBlackOnCyanWhite = ColorOptions{ IndexColumn: text.Colors{text.BgHiCyan, text.FgBlack}, Footer: text.Colors{text.BgCyan, text.FgBlack}, Header: text.Colors{text.BgHiCyan, text.FgBlack}, Row: text.Colors{text.BgHiWhite, text.FgBlack}, RowAlternate: text.Colors{text.BgWhite, text.FgBlack}, } // ColorOptionsBlackOnGreenWhite renders Black text on Green/White // background. ColorOptionsBlackOnGreenWhite = ColorOptions{ IndexColumn: text.Colors{text.BgHiGreen, text.FgBlack}, Footer: text.Colors{text.BgGreen, text.FgBlack}, Header: text.Colors{text.BgHiGreen, text.FgBlack}, Row: text.Colors{text.BgHiWhite, text.FgBlack}, RowAlternate: text.Colors{text.BgWhite, text.FgBlack}, } // ColorOptionsBlackOnMagentaWhite renders Black text on Magenta/White // background. ColorOptionsBlackOnMagentaWhite = ColorOptions{ IndexColumn: text.Colors{text.BgHiMagenta, text.FgBlack}, Footer: text.Colors{text.BgMagenta, text.FgBlack}, Header: text.Colors{text.BgHiMagenta, text.FgBlack}, Row: text.Colors{text.BgHiWhite, text.FgBlack}, RowAlternate: text.Colors{text.BgWhite, text.FgBlack}, } // ColorOptionsBlackOnRedWhite renders Black text on Red/White background. ColorOptionsBlackOnRedWhite = ColorOptions{ IndexColumn: text.Colors{text.BgHiRed, text.FgBlack}, Footer: text.Colors{text.BgRed, text.FgBlack}, Header: text.Colors{text.BgHiRed, text.FgBlack}, Row: text.Colors{text.BgHiWhite, text.FgBlack}, RowAlternate: text.Colors{text.BgWhite, text.FgBlack}, } // ColorOptionsBlackOnYellowWhite renders Black text on Yellow/White // background. ColorOptionsBlackOnYellowWhite = ColorOptions{ IndexColumn: text.Colors{text.BgHiYellow, text.FgBlack}, Footer: text.Colors{text.BgYellow, text.FgBlack}, Header: text.Colors{text.BgHiYellow, text.FgBlack}, Row: text.Colors{text.BgHiWhite, text.FgBlack}, RowAlternate: text.Colors{text.BgWhite, text.FgBlack}, } // ColorOptionsBlueWhiteOnBlack renders Blue/White text on Black background. ColorOptionsBlueWhiteOnBlack = ColorOptions{ IndexColumn: text.Colors{text.FgHiBlue, text.BgHiBlack}, Footer: text.Colors{text.FgBlue, text.BgHiBlack}, Header: text.Colors{text.FgHiBlue, text.BgHiBlack}, Row: text.Colors{text.FgHiWhite, text.BgBlack}, RowAlternate: text.Colors{text.FgWhite, text.BgBlack}, } // ColorOptionsCyanWhiteOnBlack renders Cyan/White text on Black background. ColorOptionsCyanWhiteOnBlack = ColorOptions{ IndexColumn: text.Colors{text.FgHiCyan, text.BgHiBlack}, Footer: text.Colors{text.FgCyan, text.BgHiBlack}, Header: text.Colors{text.FgHiCyan, text.BgHiBlack}, Row: text.Colors{text.FgHiWhite, text.BgBlack}, RowAlternate: text.Colors{text.FgWhite, text.BgBlack}, } // ColorOptionsGreenWhiteOnBlack renders Green/White text on Black // background. ColorOptionsGreenWhiteOnBlack = ColorOptions{ IndexColumn: text.Colors{text.FgHiGreen, text.BgHiBlack}, Footer: text.Colors{text.FgGreen, text.BgHiBlack}, Header: text.Colors{text.FgHiGreen, text.BgHiBlack}, Row: text.Colors{text.FgHiWhite, text.BgBlack}, RowAlternate: text.Colors{text.FgWhite, text.BgBlack}, } // ColorOptionsMagentaWhiteOnBlack renders Magenta/White text on Black // background. ColorOptionsMagentaWhiteOnBlack = ColorOptions{ IndexColumn: text.Colors{text.FgHiMagenta, text.BgHiBlack}, Footer: text.Colors{text.FgMagenta, text.BgHiBlack}, Header: text.Colors{text.FgHiMagenta, text.BgHiBlack}, Row: text.Colors{text.FgHiWhite, text.BgBlack}, RowAlternate: text.Colors{text.FgWhite, text.BgBlack}, } // ColorOptionsRedWhiteOnBlack renders Red/White text on Black background. ColorOptionsRedWhiteOnBlack = ColorOptions{ IndexColumn: text.Colors{text.FgHiRed, text.BgHiBlack}, Footer: text.Colors{text.FgRed, text.BgHiBlack}, Header: text.Colors{text.FgHiRed, text.BgHiBlack}, Row: text.Colors{text.FgHiWhite, text.BgBlack}, RowAlternate: text.Colors{text.FgWhite, text.BgBlack}, } // ColorOptionsYellowWhiteOnBlack renders Yellow/White text on Black // background. ColorOptionsYellowWhiteOnBlack = ColorOptions{ IndexColumn: text.Colors{text.FgHiYellow, text.BgHiBlack}, Footer: text.Colors{text.FgYellow, text.BgHiBlack}, Header: text.Colors{text.FgHiYellow, text.BgHiBlack}, Row: text.Colors{text.FgHiWhite, text.BgBlack}, RowAlternate: text.Colors{text.FgWhite, text.BgBlack}, } ) // FormatOptions defines the text-formatting to perform on parts of the Table. type FormatOptions struct { Footer text.Format // footer row(s) text format Header text.Format // header row(s) text format Row text.Format // (data) row(s) text format } var ( // FormatOptionsDefault defines sensible formatting options. FormatOptionsDefault = FormatOptions{ Footer: text.FormatUpper, Header: text.FormatUpper, Row: text.FormatDefault, } ) // HTMLOptions defines the global options to control HTML rendering. type HTMLOptions struct { CSSClass string // CSS class to set on the overall tag EmptyColumn string // string to replace "" columns with (entire content being "") EscapeText bool // escape text into HTML-safe content? Newline string // string to replace "\n" characters with } var ( // DefaultHTMLOptions defines sensible HTML rendering defaults. DefaultHTMLOptions = HTMLOptions{ CSSClass: DefaultHTMLCSSClass, EmptyColumn: " ", EscapeText: true, Newline: "
          ", } ) // Options defines the global options that determine how the Table is // rendered. type Options struct { // DrawBorder enables or disables drawing the border around the Table. // Example of a table where it is disabled: // # │ FIRST NAME │ LAST NAME │ SALARY │ // ─────┼────────────┼───────────┼────────┼───────────────────────────── // 1 │ Arya │ Stark │ 3000 │ // 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! // 300 │ Tyrion │ Lannister │ 5000 │ // ─────┼────────────┼───────────┼────────┼───────────────────────────── // │ │ TOTAL │ 10000 │ DrawBorder bool // SeparateColumns enables or disable drawing border between columns. // Example of a table where it is disabled: // ┌─────────────────────────────────────────────────────────────────┐ // │ # FIRST NAME LAST NAME SALARY │ // ├─────────────────────────────────────────────────────────────────┤ // │ 1 Arya Stark 3000 │ // │ 20 Jon Snow 2000 You know nothing, Jon Snow! │ // │ 300 Tyrion Lannister 5000 │ // │ TOTAL 10000 │ // └─────────────────────────────────────────────────────────────────┘ SeparateColumns bool // SeparateFooter enables or disable drawing border between the footer and // the rows. Example of a table where it is disabled: // ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐ // │ # │ FIRST NAME │ LAST NAME │ SALARY │ │ // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ // │ 1 │ Arya │ Stark │ 3000 │ │ // │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ // │ 300 │ Tyrion │ Lannister │ 5000 │ │ // │ │ │ TOTAL │ 10000 │ │ // └─────┴────────────┴───────────┴────────┴─────────────────────────────┘ SeparateFooter bool // SeparateHeader enables or disable drawing border between the header and // the rows. Example of a table where it is disabled: // ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐ // │ # │ FIRST NAME │ LAST NAME │ SALARY │ │ // │ 1 │ Arya │ Stark │ 3000 │ │ // │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ // │ 300 │ Tyrion │ Lannister │ 5000 │ │ // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ // │ │ │ TOTAL │ 10000 │ │ // └─────┴────────────┴───────────┴────────┴─────────────────────────────┘ SeparateHeader bool // SeparateRows enables or disables drawing separators between each row. // Example of a table where it is enabled: // ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐ // │ # │ FIRST NAME │ LAST NAME │ SALARY │ │ // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ // │ 1 │ Arya │ Stark │ 3000 │ │ // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ // │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ // │ 300 │ Tyrion │ Lannister │ 5000 │ │ // ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤ // │ │ │ TOTAL │ 10000 │ │ // └─────┴────────────┴───────────┴────────┴─────────────────────────────┘ SeparateRows bool } var ( // OptionsDefault defines sensible global options. OptionsDefault = Options{ DrawBorder: true, SeparateColumns: true, SeparateFooter: true, SeparateHeader: true, SeparateRows: false, } // OptionsNoBorders sets up a table without any borders. OptionsNoBorders = Options{ DrawBorder: false, SeparateColumns: true, SeparateFooter: true, SeparateHeader: true, SeparateRows: false, } // OptionsNoBordersAndSeparators sets up a table without any borders or // separators. OptionsNoBordersAndSeparators = Options{ DrawBorder: false, SeparateColumns: false, SeparateFooter: false, SeparateHeader: false, SeparateRows: false, } ) // TitleOptions defines the way the title text is to be rendered. type TitleOptions struct { Align text.Align Colors text.Colors Format text.Format } var ( // TitleOptionsDefault defines sensible title options - basically NONE. TitleOptionsDefault = TitleOptions{} // TitleOptionsBright renders Bright Bold text on Dark background. TitleOptionsBright = TitleOptionsBlackOnCyan // TitleOptionsDark renders Dark Bold text on Bright background. TitleOptionsDark = TitleOptionsCyanOnBlack // TitleOptionsBlackOnBlue renders Black text on Blue background. TitleOptionsBlackOnBlue = TitleOptions{ Colors: append(ColorOptionsBlackOnBlueWhite.Header, text.Bold), } // TitleOptionsBlackOnCyan renders Black Bold text on Cyan background. TitleOptionsBlackOnCyan = TitleOptions{ Colors: append(ColorOptionsBlackOnCyanWhite.Header, text.Bold), } // TitleOptionsBlackOnGreen renders Black Bold text onGreen background. TitleOptionsBlackOnGreen = TitleOptions{ Colors: append(ColorOptionsBlackOnGreenWhite.Header, text.Bold), } // TitleOptionsBlackOnMagenta renders Black Bold text on Magenta background. TitleOptionsBlackOnMagenta = TitleOptions{ Colors: append(ColorOptionsBlackOnMagentaWhite.Header, text.Bold), } // TitleOptionsBlackOnRed renders Black Bold text on Red background. TitleOptionsBlackOnRed = TitleOptions{ Colors: append(ColorOptionsBlackOnRedWhite.Header, text.Bold), } // TitleOptionsBlackOnYellow renders Black Bold text on Yellow background. TitleOptionsBlackOnYellow = TitleOptions{ Colors: append(ColorOptionsBlackOnYellowWhite.Header, text.Bold), } // TitleOptionsBlueOnBlack renders Blue Bold text on Black background. TitleOptionsBlueOnBlack = TitleOptions{ Colors: append(ColorOptionsBlueWhiteOnBlack.Header, text.Bold), } // TitleOptionsCyanOnBlack renders Cyan Bold text on Black background. TitleOptionsCyanOnBlack = TitleOptions{ Colors: append(ColorOptionsCyanWhiteOnBlack.Header, text.Bold), } // TitleOptionsGreenOnBlack renders Green Bold text on Black background. TitleOptionsGreenOnBlack = TitleOptions{ Colors: append(ColorOptionsGreenWhiteOnBlack.Header, text.Bold), } // TitleOptionsMagentaOnBlack renders Magenta Bold text on Black background. TitleOptionsMagentaOnBlack = TitleOptions{ Colors: append(ColorOptionsMagentaWhiteOnBlack.Header, text.Bold), } // TitleOptionsRedOnBlack renders Red Bold text on Black background. TitleOptionsRedOnBlack = TitleOptions{ Colors: append(ColorOptionsRedWhiteOnBlack.Header, text.Bold), } // TitleOptionsYellowOnBlack renders Yellow Bold text on Black background. TitleOptionsYellowOnBlack = TitleOptions{ Colors: append(ColorOptionsYellowWhiteOnBlack.Header, text.Bold), } ) go-pretty-6.2.4/table/table.go000066400000000000000000000670651407250454200162030ustar00rootroot00000000000000package table import ( "fmt" "io" "strings" "github.com/jedib0t/go-pretty/v6/text" ) // Row defines a single row in the Table. type Row []interface{} // RowPainter is a custom function that takes a Row as input and returns the // text.Colors{} to use on the entire row type RowPainter func(row Row) text.Colors // rowStr defines a single row in the Table comprised of just string objects. type rowStr []string // areEqual returns true if the contents of the 2 given columns are the same func (row rowStr) areEqual(colIdx1 int, colIdx2 int) bool { return colIdx1 >= 0 && colIdx2 < len(row) && row[colIdx1] == row[colIdx2] } // Table helps print a 2-dimensional array in a human readable pretty-table. type Table struct { // allowedRowLength is the max allowed length for a row (or line of output) allowedRowLength int // enable automatic indexing of the rows and columns like a spreadsheet? autoIndex bool // autoIndexVIndexMaxLength denotes the length in chars for the last rownum autoIndexVIndexMaxLength int // caption stores the text to be rendered just below the table; and doesn't // get used when rendered as a CSV caption string // columnIsNonNumeric stores if a column contains non-numbers in all rows columnIsNonNumeric []bool // columnConfigs stores the custom-configuration for 1 or more columns columnConfigs []ColumnConfig // columnConfigMap stores the custom-configuration by column // number and is generated before rendering columnConfigMap map[int]ColumnConfig // htmlCSSClass stores the HTML CSS Class to use on the
          node htmlCSSClass string // indexColumn stores the number of the column considered as the "index" indexColumn int // maxColumnLengths stores the length of the longest line in each column maxColumnLengths []int // maxRowLength stores the length of the longest row maxRowLength int // numColumns stores the (max.) number of columns seen numColumns int // numLinesRendered keeps track of the number of lines rendered and helps in // paginating long tables numLinesRendered int // outputMirror stores an io.Writer where the "Render" functions would write outputMirror io.Writer // pageSize stores the maximum lines to render before rendering the header // again (to denote a page break) - useful when you are dealing with really // long tables pageSize int // rows stores the rows that make up the body (in string form) rows []rowStr // rowsColors stores the text.Colors over-rides for each row as defined by // rowPainter rowsColors []text.Colors // rowsConfigs stores RowConfig for each row rowsConfigMap map[int]RowConfig // rowsRaw stores the rows that make up the body rowsRaw []Row // rowsFooter stores the rows that make up the footer (in string form) rowsFooter []rowStr // rowsFooterConfigs stores RowConfig for each footer row rowsFooterConfigMap map[int]RowConfig // rowsFooterRaw stores the rows that make up the footer rowsFooterRaw []Row // rowsHeader stores the rows that make up the header (in string form) rowsHeader []rowStr // rowsHeaderConfigs stores RowConfig for each header row rowsHeaderConfigMap map[int]RowConfig // rowsHeaderRaw stores the rows that make up the header rowsHeaderRaw []Row // rowPainter is a custom function that given a Row, returns the colors to // use on the entire row rowPainter RowPainter // rowSeparator is a dummy row that contains the separator columns (dashes // that make up the separator between header/body/footer rowSeparator rowStr // separators is used to keep track of all rowIndices after which a // separator has to be rendered separators map[int]bool // sortBy stores a map of Column sortBy []SortBy // style contains all the strings used to draw the table, and more style *Style // suppressEmptyColumns hides columns which have no content on all regular // rows suppressEmptyColumns bool // title contains the text to appear above the table title string } // AppendFooter appends the row to the List of footers to render. // // Only the first item in the "config" will be tagged against this row. func (t *Table) AppendFooter(row Row, config ...RowConfig) { t.rowsFooterRaw = append(t.rowsFooterRaw, row) if len(config) > 0 { if t.rowsFooterConfigMap == nil { t.rowsFooterConfigMap = make(map[int]RowConfig) } t.rowsFooterConfigMap[len(t.rowsFooterRaw)-1] = config[0] } } // AppendHeader appends the row to the List of headers to render. // // Only the first item in the "config" will be tagged against this row. func (t *Table) AppendHeader(row Row, config ...RowConfig) { t.rowsHeaderRaw = append(t.rowsHeaderRaw, row) if len(config) > 0 { if t.rowsHeaderConfigMap == nil { t.rowsHeaderConfigMap = make(map[int]RowConfig) } t.rowsHeaderConfigMap[len(t.rowsHeaderRaw)-1] = config[0] } } // AppendRow appends the row to the List of rows to render. // // Only the first item in the "config" will be tagged against this row. func (t *Table) AppendRow(row Row, config ...RowConfig) { t.rowsRaw = append(t.rowsRaw, row) if len(config) > 0 { if t.rowsConfigMap == nil { t.rowsConfigMap = make(map[int]RowConfig) } t.rowsConfigMap[len(t.rowsRaw)-1] = config[0] } } // AppendRows appends the rows to the List of rows to render. // // Only the first item in the "config" will be tagged against all the rows. func (t *Table) AppendRows(rows []Row, config ...RowConfig) { for _, row := range rows { t.AppendRow(row, config...) } } // AppendSeparator helps render a separator row after the current last row. You // could call this function over and over, but it will be a no-op unless you // call AppendRow or AppendRows in between. Likewise, if the last thing you // append is a separator, it will not be rendered in addition to the usual table // separator. // //****************************************************************************** // Please note the following caveats: // 1. SetPageSize(): this may end up creating consecutive separator rows near // the end of a page or at the beginning of a page // 2. SortBy(): since SortBy could inherently alter the ordering of rows, the // separators may not appear after the row it was originally intended to // follow //****************************************************************************** func (t *Table) AppendSeparator() { if t.separators == nil { t.separators = make(map[int]bool) } if len(t.rowsRaw) > 0 { t.separators[len(t.rowsRaw)-1] = true } } // Length returns the number of rows to be rendered. func (t *Table) Length() int { return len(t.rowsRaw) } // ResetFooters resets and clears all the Footer rows appended earlier. func (t *Table) ResetFooters() { t.rowsFooterRaw = nil } // ResetHeaders resets and clears all the Header rows appended earlier. func (t *Table) ResetHeaders() { t.rowsHeaderRaw = nil } // ResetRows resets and clears all the rows appended earlier. func (t *Table) ResetRows() { t.rowsRaw = nil t.separators = nil } // SetAllowedRowLength sets the maximum allowed length or a row (or line of // output) when rendered as a table. Rows that are longer than this limit will // be "snipped" to the length. Length has to be a positive value to take effect. func (t *Table) SetAllowedRowLength(length int) { t.allowedRowLength = length } // SetAutoIndex adds a generated header with columns such as "A", "B", "C", etc. // and a leading column with the row number similar to what you'd see on any // spreadsheet application. NOTE: Appending a Header will void this // functionality. func (t *Table) SetAutoIndex(autoIndex bool) { t.autoIndex = autoIndex } // SetCaption sets the text to be rendered just below the table. This will not // show up when the Table is rendered as a CSV. func (t *Table) SetCaption(format string, a ...interface{}) { t.caption = fmt.Sprintf(format, a...) } // SetColumnConfigs sets the configs for each Column. func (t *Table) SetColumnConfigs(configs []ColumnConfig) { t.columnConfigs = configs } // SetHTMLCSSClass sets the the HTML CSS Class to use on the
          node // when rendering the Table in HTML format. // // Deprecated: in favor of Style().HTML.CSSClass func (t *Table) SetHTMLCSSClass(cssClass string) { t.htmlCSSClass = cssClass } // SetIndexColumn sets the given Column # as the column that has the row // "Number". Valid values range from 1 to N. Note that this is not 0-indexed. func (t *Table) SetIndexColumn(colNum int) { t.indexColumn = colNum } // SetOutputMirror sets an io.Writer for all the Render functions to "Write" to // in addition to returning a string. func (t *Table) SetOutputMirror(mirror io.Writer) { t.outputMirror = mirror } // SetPageSize sets the maximum number of lines to render before rendering the // header rows again. This can be useful when dealing with tables containing a // long list of rows that can span pages. Please note that the pagination logic // will not consider Header/Footer lines for paging. func (t *Table) SetPageSize(numLines int) { t.pageSize = numLines } // SetRowPainter sets the RowPainter function which determines the colors to use // on a row. Before rendering, this function is invoked on all rows and the // color of each row is determined. This color takes precedence over other ways // to set color (ColumnConfig.Color*, SetColor*()). func (t *Table) SetRowPainter(painter RowPainter) { t.rowPainter = painter } // SetStyle overrides the DefaultStyle with the provided one. func (t *Table) SetStyle(style Style) { t.style = &style } // SetTitle sets the title text to be rendered above the table. func (t *Table) SetTitle(format string, a ...interface{}) { t.title = fmt.Sprintf(format, a...) } // SortBy sets the rules for sorting the Rows in the order specified. i.e., the // first SortBy instruction takes precedence over the second and so on. Any // duplicate instructions on the same column will be discarded while sorting. func (t *Table) SortBy(sortBy []SortBy) { t.sortBy = sortBy } // Style returns the current style. func (t *Table) Style() *Style { if t.style == nil { tempStyle := StyleDefault t.style = &tempStyle } return t.style } // SuppressEmptyColumns hides columns when the column is empty in ALL the // regular rows. func (t *Table) SuppressEmptyColumns() { t.suppressEmptyColumns = true } func (t *Table) analyzeAndStringify(row Row, hint renderHint) rowStr { // update t.numColumns if this row is the longest seen till now if len(row) > t.numColumns { // init the slice for the first time; and pad it the rest of the time if t.numColumns == 0 { t.columnIsNonNumeric = make([]bool, len(row)) } else { t.columnIsNonNumeric = append(t.columnIsNonNumeric, make([]bool, len(row)-t.numColumns)...) } // update t.numColumns t.numColumns = len(row) } // convert each column to string and figure out if it has non-numeric data rowOut := make(rowStr, len(row)) for colIdx, col := range row { // if the column is not a number, keep track of it if !hint.isHeaderRow && !hint.isFooterRow && !t.columnIsNonNumeric[colIdx] && !isNumber(col) { t.columnIsNonNumeric[colIdx] = true } // convert to a string and store it in the row var colStr string if transformer := t.getColumnTransformer(colIdx, hint); transformer != nil { colStr = transformer(col) } else if colStrVal, ok := col.(string); ok { colStr = colStrVal } else { colStr = fmt.Sprint(col) } if strings.Contains(colStr, "\t") { colStr = strings.Replace(colStr, "\t", " ", -1) } if strings.Contains(colStr, "\r") { colStr = strings.Replace(colStr, "\r", "", -1) } rowOut[colIdx] = colStr } return rowOut } func (t *Table) getAlign(colIdx int, hint renderHint) text.Align { align := text.AlignDefault if cfg, ok := t.columnConfigMap[colIdx]; ok { if hint.isHeaderRow { align = cfg.AlignHeader } else if hint.isFooterRow { align = cfg.AlignFooter } else { align = cfg.Align } } if align == text.AlignDefault { if !t.columnIsNonNumeric[colIdx] { align = text.AlignRight } else if hint.isAutoIndexRow { align = text.AlignCenter } } return align } func (t *Table) getAutoIndexColumnIDs() rowStr { row := make(rowStr, t.numColumns) for colIdx := range row { row[colIdx] = AutoIndexColumnID(colIdx) } return row } func (t *Table) getBorderColors(hint renderHint) text.Colors { if hint.isFooterRow { return t.style.Color.Footer } else if t.autoIndex { return t.style.Color.IndexColumn } return t.style.Color.Header } func (t *Table) getColumnColors(colIdx int, hint renderHint) text.Colors { if t.rowPainter != nil && hint.isRegularRow() && !t.isIndexColumn(colIdx, hint) { colors := t.rowsColors[hint.rowNumber-1] if colors != nil { return colors } } if cfg, ok := t.columnConfigMap[colIdx]; ok { if hint.isSeparatorRow { return nil } else if hint.isHeaderRow { return cfg.ColorsHeader } else if hint.isFooterRow { return cfg.ColorsFooter } return cfg.Colors } return nil } func (t *Table) getColumnSeparator(row rowStr, colIdx int, hint renderHint) string { separator := t.style.Box.MiddleVertical if hint.isSeparatorRow { if hint.isBorderTop { if t.shouldMergeCellsHorizontallyBelow(row, colIdx, hint) { separator = t.style.Box.MiddleHorizontal } else { separator = t.style.Box.TopSeparator } } else if hint.isBorderBottom { if t.shouldMergeCellsHorizontallyAbove(row, colIdx, hint) { separator = t.style.Box.MiddleHorizontal } else { separator = t.style.Box.BottomSeparator } } else { separator = t.getColumnSeparatorNonBorder( t.shouldMergeCellsHorizontallyAbove(row, colIdx, hint), t.shouldMergeCellsHorizontallyBelow(row, colIdx, hint), colIdx, hint, ) } } return separator } func (t *Table) getColumnSeparatorNonBorder(mergeCellsAbove bool, mergeCellsBelow bool, colIdx int, hint renderHint) string { mergeNextCol := t.shouldMergeCellsVertically(colIdx, hint) if hint.isAutoIndexColumn { return t.getColumnSeparatorNonBorderAutoIndex(mergeNextCol, hint) } mergeCurrCol := t.shouldMergeCellsVertically(colIdx-1, hint) return t.getColumnSeparatorNonBorderNonAutoIndex(mergeCellsAbove, mergeCellsBelow, mergeCurrCol, mergeNextCol) } func (t *Table) getColumnSeparatorNonBorderAutoIndex(mergeNextCol bool, hint renderHint) string { if hint.isHeaderOrFooterSeparator() { if mergeNextCol { return t.style.Box.MiddleVertical } return t.style.Box.LeftSeparator } else if mergeNextCol { return t.style.Box.RightSeparator } return t.style.Box.MiddleSeparator } func (t *Table) getColumnSeparatorNonBorderNonAutoIndex(mergeCellsAbove bool, mergeCellsBelow bool, mergeCurrCol bool, mergeNextCol bool) string { if mergeCellsAbove && mergeCellsBelow && mergeCurrCol && mergeNextCol { return t.style.Box.EmptySeparator } else if mergeCellsAbove && mergeCellsBelow { return t.style.Box.MiddleHorizontal } else if mergeCellsAbove { return t.style.Box.TopSeparator } else if mergeCellsBelow { return t.style.Box.BottomSeparator } else if mergeCurrCol && mergeNextCol { return t.style.Box.MiddleVertical } else if mergeCurrCol { return t.style.Box.LeftSeparator } return t.style.Box.MiddleSeparator } func (t *Table) getColumnTransformer(colIdx int, hint renderHint) text.Transformer { var transformer text.Transformer if cfg, ok := t.columnConfigMap[colIdx]; ok { if hint.isHeaderRow { transformer = cfg.TransformerHeader } else if hint.isFooterRow { transformer = cfg.TransformerFooter } else { transformer = cfg.Transformer } } return transformer } func (t *Table) getColumnWidthMax(colIdx int) int { if cfg, ok := t.columnConfigMap[colIdx]; ok { return cfg.WidthMax } return 0 } func (t *Table) getColumnWidthMin(colIdx int) int { if cfg, ok := t.columnConfigMap[colIdx]; ok { return cfg.WidthMin } return 0 } func (t *Table) getFormat(hint renderHint) text.Format { if hint.isSeparatorRow { return text.FormatDefault } else if hint.isHeaderRow { return t.style.Format.Header } else if hint.isFooterRow { return t.style.Format.Footer } return t.style.Format.Row } func (t *Table) getRow(rowIdx int, hint renderHint) rowStr { switch { case hint.isHeaderRow: if rowIdx >= 0 && rowIdx < len(t.rowsHeader) { return t.rowsHeader[rowIdx] } case hint.isFooterRow: if rowIdx >= 0 && rowIdx < len(t.rowsFooter) { return t.rowsFooter[rowIdx] } default: if rowIdx >= 0 && rowIdx < len(t.rows) { return t.rows[rowIdx] } } return rowStr{} } func (t *Table) getRowConfig(hint renderHint) RowConfig { rowIdx := hint.rowNumber - 1 if rowIdx < 0 { rowIdx = 0 } switch { case hint.isHeaderRow: return t.rowsHeaderConfigMap[rowIdx] case hint.isFooterRow: return t.rowsFooterConfigMap[rowIdx] default: return t.rowsConfigMap[rowIdx] } } func (t *Table) getSeparatorColors(hint renderHint) text.Colors { if hint.isHeaderRow { return t.style.Color.Header } else if hint.isFooterRow { return t.style.Color.Footer } else if hint.isAutoIndexColumn { return t.style.Color.IndexColumn } else if hint.rowNumber > 0 && hint.rowNumber%2 == 0 { return t.style.Color.RowAlternate } return t.style.Color.Row } func (t *Table) getVAlign(colIdx int, hint renderHint) text.VAlign { vAlign := text.VAlignDefault if cfg, ok := t.columnConfigMap[colIdx]; ok { if hint.isHeaderRow { vAlign = cfg.VAlignHeader } else if hint.isFooterRow { vAlign = cfg.VAlignFooter } else { vAlign = cfg.VAlign } } return vAlign } func (t *Table) initForRender() { // pick a default style if none was set until now t.Style() // initialize the column configs and normalize them t.initForRenderColumnConfigs() // initialize and stringify all the raw rows t.initForRenderRows() // find the longest continuous line in each column t.initForRenderColumnLengths() // generate a separator row and calculate maximum row length t.initForRenderRowSeparator() // reset the counter for the number of lines rendered t.numLinesRendered = 0 } func (t *Table) initForRenderColumnConfigs() { findColumnNumber := func(row Row, colName string) int { for colIdx, col := range row { if fmt.Sprint(col) == colName { return colIdx + 1 } } return 0 } t.columnConfigMap = map[int]ColumnConfig{} for _, colCfg := range t.columnConfigs { // find the column number if none provided; this logic can work only if // a header row is present and has a column with the given name if colCfg.Number == 0 { for _, row := range t.rowsHeaderRaw { colCfg.Number = findColumnNumber(row, colCfg.Name) if colCfg.Number > 0 { break } } } if colCfg.Number > 0 { t.columnConfigMap[colCfg.Number-1] = colCfg } } } func (t *Table) initForRenderColumnLengths() { var findMaxColumnLengths = func(rows []rowStr) { for _, row := range rows { for colIdx, colStr := range row { longestLineLen := text.LongestLineLen(colStr) if longestLineLen > t.maxColumnLengths[colIdx] { t.maxColumnLengths[colIdx] = longestLineLen } } } } t.maxColumnLengths = make([]int, t.numColumns) findMaxColumnLengths(t.rowsHeader) findMaxColumnLengths(t.rows) findMaxColumnLengths(t.rowsFooter) // restrict the column lengths if any are over or under the limits for colIdx := range t.maxColumnLengths { maxWidth := t.getColumnWidthMax(colIdx) if maxWidth > 0 && t.maxColumnLengths[colIdx] > maxWidth { t.maxColumnLengths[colIdx] = maxWidth } minWidth := t.getColumnWidthMin(colIdx) if minWidth > 0 && t.maxColumnLengths[colIdx] < minWidth { t.maxColumnLengths[colIdx] = minWidth } } } func (t *Table) initForRenderHideColumns() { // if there is nothing to hide, return fast hasHiddenColumns := false for _, cc := range t.columnConfigMap { if cc.Hidden { hasHiddenColumns = true break } } if !hasHiddenColumns { return } colIdxMap := make(map[int]int) numColumns := 0 _hideColumns := func(rows []rowStr) []rowStr { var rsp []rowStr for _, row := range rows { var rowNew rowStr for colIdx, col := range row { cc := t.columnConfigMap[colIdx] if !cc.Hidden { rowNew = append(rowNew, col) colIdxMap[colIdx] = len(rowNew) - 1 } } if len(rowNew) > numColumns { numColumns = len(rowNew) } rsp = append(rsp, rowNew) } return rsp } // hide columns as directed t.rows = _hideColumns(t.rows) t.rowsFooter = _hideColumns(t.rowsFooter) t.rowsHeader = _hideColumns(t.rowsHeader) // reset numColumns to the new number of columns t.numColumns = numColumns // re-create columnIsNonNumeric with new column indices columnIsNonNumeric := make([]bool, t.numColumns) for oldColIdx, nonNumeric := range t.columnIsNonNumeric { if newColIdx, ok := colIdxMap[oldColIdx]; ok { columnIsNonNumeric[newColIdx] = nonNumeric } } t.columnIsNonNumeric = columnIsNonNumeric // re-create columnConfigMap with new column indices columnConfigMap := make(map[int]ColumnConfig) for oldColIdx, cc := range t.columnConfigMap { if newColIdx, ok := colIdxMap[oldColIdx]; ok { columnConfigMap[newColIdx] = cc } } t.columnConfigMap = columnConfigMap } func (t *Table) initForRenderRows() { t.reset() // auto-index: calc the index column's max length t.autoIndexVIndexMaxLength = len(fmt.Sprint(len(t.rowsRaw))) // stringify all the rows to make it easy to render if t.rowPainter != nil { t.rowsColors = make([]text.Colors, len(t.rowsRaw)) } t.rows = t.initForRenderRowsStringify(t.rowsRaw, renderHint{}) t.rowsFooter = t.initForRenderRowsStringify(t.rowsFooterRaw, renderHint{isFooterRow: true}) t.rowsHeader = t.initForRenderRowsStringify(t.rowsHeaderRaw, renderHint{isHeaderRow: true}) // sort the rows as requested t.initForRenderSortRows() // suppress columns without any content t.initForRenderSuppressColumns() // strip out hidden columns t.initForRenderHideColumns() } func (t *Table) initForRenderRowsStringify(rows []Row, hint renderHint) []rowStr { rowsStr := make([]rowStr, len(rows)) for idx, row := range rows { if t.rowPainter != nil && hint.isRegularRow() { t.rowsColors[idx] = t.rowPainter(row) } rowsStr[idx] = t.analyzeAndStringify(row, hint) } return rowsStr } func (t *Table) initForRenderRowSeparator() { t.maxRowLength = 0 if t.autoIndex { t.maxRowLength += text.RuneCount(t.style.Box.PaddingLeft) t.maxRowLength += len(fmt.Sprint(len(t.rows))) t.maxRowLength += text.RuneCount(t.style.Box.PaddingRight) if t.style.Options.SeparateColumns { t.maxRowLength += text.RuneCount(t.style.Box.MiddleSeparator) } } if t.style.Options.SeparateColumns { t.maxRowLength += text.RuneCount(t.style.Box.MiddleSeparator) * (t.numColumns - 1) } t.rowSeparator = make(rowStr, t.numColumns) for colIdx, maxColumnLength := range t.maxColumnLengths { maxColumnLength += text.RuneCount(t.style.Box.PaddingLeft + t.style.Box.PaddingRight) t.maxRowLength += maxColumnLength t.rowSeparator[colIdx] = text.RepeatAndTrim(t.style.Box.MiddleHorizontal, maxColumnLength) } if t.style.Options.DrawBorder { t.maxRowLength += text.RuneCount(t.style.Box.Left + t.style.Box.Right) } } func (t *Table) initForRenderSortRows() { if len(t.sortBy) == 0 { return } // sort the rows sortedRowIndices := t.getSortedRowIndices() sortedRows := make([]rowStr, len(t.rows)) for idx := range t.rows { sortedRows[idx] = t.rows[sortedRowIndices[idx]] } t.rows = sortedRows // sort the rowsColors if len(t.rowsColors) > 0 { sortedRowsColors := make([]text.Colors, len(t.rows)) for idx := range t.rows { sortedRowsColors[idx] = t.rowsColors[sortedRowIndices[idx]] } t.rowsColors = sortedRowsColors } } func (t *Table) initForRenderSuppressColumns() { shouldSuppressColumn := func(colIdx int) bool { for _, row := range t.rows { if colIdx < len(row) && row[colIdx] != "" { return false } } return true } if t.suppressEmptyColumns { for colIdx := 0; colIdx < t.numColumns; colIdx++ { if shouldSuppressColumn(colIdx) { cc := t.columnConfigMap[colIdx] cc.Hidden = true t.columnConfigMap[colIdx] = cc } } } } func (t *Table) isIndexColumn(colIdx int, hint renderHint) bool { return t.indexColumn == colIdx+1 || hint.isAutoIndexColumn } func (t *Table) render(out *strings.Builder) string { outStr := out.String() if t.outputMirror != nil && len(outStr) > 0 { _, _ = t.outputMirror.Write([]byte(outStr)) _, _ = t.outputMirror.Write([]byte("\n")) } return outStr } func (t *Table) reset() { t.autoIndexVIndexMaxLength = 0 t.columnIsNonNumeric = nil t.maxColumnLengths = nil t.maxRowLength = 0 t.numColumns = 0 t.rowsColors = nil t.rowSeparator = nil t.rows = nil t.rowsFooter = nil t.rowsHeader = nil } func (t *Table) shouldMergeCellsHorizontallyAbove(row rowStr, colIdx int, hint renderHint) bool { if hint.isAutoIndexColumn || hint.isAutoIndexRow { return false } rowConfig := t.getRowConfig(hint) if hint.isSeparatorRow { if hint.isHeaderRow && hint.rowNumber == 1 { rowConfig = t.getRowConfig(hint) row = t.getRow(hint.rowNumber-1, hint) } else if hint.isFooterRow && hint.isFirstRow { rowConfig = t.getRowConfig(renderHint{isLastRow: true, rowNumber: len(t.rows)}) row = t.getRow(len(t.rows)-1, renderHint{}) } else if hint.isFooterRow && hint.isBorderBottom { row = t.getRow(len(t.rowsFooter)-1, renderHint{isFooterRow: true}) } else { row = t.getRow(hint.rowNumber-1, hint) } } if rowConfig.AutoMerge { return row.areEqual(colIdx-1, colIdx) } return false } func (t *Table) shouldMergeCellsHorizontallyBelow(row rowStr, colIdx int, hint renderHint) bool { if hint.isAutoIndexColumn || hint.isAutoIndexRow { return false } var rowConfig RowConfig if hint.isSeparatorRow { if hint.isHeaderRow && hint.rowNumber == 0 { rowConfig = t.getRowConfig(renderHint{isHeaderRow: true, rowNumber: 1}) row = t.getRow(0, hint) } else if hint.isHeaderRow && hint.isLastRow { rowConfig = t.getRowConfig(renderHint{rowNumber: 1}) row = t.getRow(0, renderHint{}) } else if hint.isFooterRow && hint.rowNumber >= 0 { rowConfig = t.getRowConfig(renderHint{isFooterRow: true, rowNumber: 1}) row = t.getRow(hint.rowNumber, renderHint{isFooterRow: true}) } else if hint.isRegularRow() { rowConfig = t.getRowConfig(renderHint{rowNumber: hint.rowNumber + 1}) row = t.getRow(hint.rowNumber, renderHint{}) } } if rowConfig.AutoMerge { return row.areEqual(colIdx-1, colIdx) } return false } func (t *Table) shouldMergeCellsVertically(colIdx int, hint renderHint) bool { if t.columnConfigMap[colIdx].AutoMerge && colIdx < t.numColumns { if hint.isSeparatorRow { rowPrev := t.getRow(hint.rowNumber-1, hint) rowNext := t.getRow(hint.rowNumber, hint) if colIdx < len(rowPrev) && colIdx < len(rowNext) { return rowPrev[colIdx] == rowNext[colIdx] || "" == rowNext[colIdx] } } else { rowPrev := t.getRow(hint.rowNumber-2, hint) rowCurr := t.getRow(hint.rowNumber-1, hint) if colIdx < len(rowPrev) && colIdx < len(rowCurr) { return rowPrev[colIdx] == rowCurr[colIdx] || "" == rowCurr[colIdx] } } } return false } // renderHint has hints for the Render*() logic type renderHint struct { isAutoIndexColumn bool // auto-index column? isAutoIndexRow bool // auto-index row? isBorderBottom bool // bottom-border? isBorderTop bool // top-border? isFirstRow bool // first-row of header/footer/regular-rows? isFooterRow bool // footer row? isHeaderRow bool // header row? isLastLineOfRow bool // last-line of the current row? isLastRow bool // last-row of header/footer/regular-rows? isSeparatorRow bool // separator row? rowLineNumber int // the line number for a multi-line row rowNumber int // the row number/index } func (h *renderHint) isRegularRow() bool { return !h.isHeaderRow && !h.isFooterRow } func (h *renderHint) isHeaderOrFooterSeparator() bool { return h.isSeparatorRow && !h.isBorderBottom && !h.isBorderTop && ((h.isHeaderRow && !h.isLastRow) || (h.isFooterRow && (!h.isFirstRow || h.rowNumber > 0))) } func (h *renderHint) isLastLineOfLastRow() bool { return h.isLastLineOfRow && h.isLastRow } go-pretty-6.2.4/table/table_test.go000066400000000000000000000324321407250454200172300ustar00rootroot00000000000000package table import ( "strings" "testing" "unicode/utf8" "github.com/jedib0t/go-pretty/v6/text" "github.com/stretchr/testify/assert" ) var ( testCaption = "A Song of Ice and Fire" testColor = text.Colors{text.FgGreen} testColorHiRedBold = text.Colors{text.FgHiRed, text.Bold} testColorHiBlueBold = text.Colors{text.FgHiBlue, text.Bold} testCSSClass = "test-css-class" testFooter = Row{"", "", "Total", 10000} testFooterMultiLine = Row{"", "", "Total\nSalary", 10000} testHeader = Row{"#", "First Name", "Last Name", "Salary"} testHeaderMultiLine = Row{"#", "First\nName", "Last\nName", "Salary"} testRows = []Row{ {1, "Arya", "Stark", 3000}, {20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"}, {300, "Tyrion", "Lannister", 5000}, } testRowMultiLine = Row{0, "Winter", "Is", 0, "Coming.\r\nThe North Remembers!\nThis is known."} testRowNewLines = Row{0, "Valar", "Morghulis", 0, "Faceless\nMen"} testRowPipes = Row{0, "Valar", "Morghulis", 0, "Faceless|Men"} testRowTabs = Row{0, "Valar", "Morghulis", 0, "Faceless\tMen"} testTitle1 = "Game of Thrones" testTitle2 = "When you play the Game of Thrones, you win or you die. There is no middle ground." ) func init() { text.EnableColors() } type myMockOutputMirror struct { mirroredOutput string } func (t *myMockOutputMirror) Write(p []byte) (n int, err error) { t.mirroredOutput += string(p) return len(p), nil } func TestNewWriter(t *testing.T) { tw := NewWriter() assert.NotNil(t, tw.Style()) assert.Equal(t, StyleDefault, *tw.Style()) tw.SetStyle(StyleBold) assert.NotNil(t, tw.Style()) assert.Equal(t, StyleBold, *tw.Style()) } func TestTable_AppendFooter(t *testing.T) { table := Table{} assert.Equal(t, 0, len(table.rowsFooterRaw)) table.AppendFooter([]interface{}{}) assert.Equal(t, 0, table.Length()) assert.Equal(t, 1, len(table.rowsFooterRaw)) assert.Equal(t, 0, len(table.rowsHeaderRaw)) table.AppendFooter([]interface{}{}) assert.Equal(t, 0, table.Length()) assert.Equal(t, 2, len(table.rowsFooterRaw)) assert.Equal(t, 0, len(table.rowsHeaderRaw)) table.AppendFooter([]interface{}{}, RowConfig{AutoMerge: true}) assert.Equal(t, 0, table.Length()) assert.Equal(t, 3, len(table.rowsFooterRaw)) assert.Equal(t, 0, len(table.rowsHeaderRaw)) assert.False(t, table.rowsFooterConfigMap[0].AutoMerge) assert.False(t, table.rowsFooterConfigMap[1].AutoMerge) assert.True(t, table.rowsFooterConfigMap[2].AutoMerge) } func TestTable_AppendHeader(t *testing.T) { table := Table{} assert.Equal(t, 0, len(table.rowsHeaderRaw)) table.AppendHeader([]interface{}{}) assert.Equal(t, 0, table.Length()) assert.Equal(t, 0, len(table.rowsFooterRaw)) assert.Equal(t, 1, len(table.rowsHeaderRaw)) table.AppendHeader([]interface{}{}) assert.Equal(t, 0, table.Length()) assert.Equal(t, 0, len(table.rowsFooterRaw)) assert.Equal(t, 2, len(table.rowsHeaderRaw)) table.AppendHeader([]interface{}{}, RowConfig{AutoMerge: true}) assert.Equal(t, 0, table.Length()) assert.Equal(t, 0, len(table.rowsFooterRaw)) assert.Equal(t, 3, len(table.rowsHeaderRaw)) assert.False(t, table.rowsHeaderConfigMap[0].AutoMerge) assert.False(t, table.rowsHeaderConfigMap[1].AutoMerge) assert.True(t, table.rowsHeaderConfigMap[2].AutoMerge) } func TestTable_AppendRow(t *testing.T) { table := Table{} assert.Equal(t, 0, table.Length()) table.AppendRow([]interface{}{}) assert.Equal(t, 1, table.Length()) assert.Equal(t, 0, len(table.rowsFooter)) assert.Equal(t, 0, len(table.rowsHeader)) table.AppendRow([]interface{}{}) assert.Equal(t, 2, table.Length()) assert.Equal(t, 0, len(table.rowsFooter)) assert.Equal(t, 0, len(table.rowsHeader)) table.AppendRow([]interface{}{}, RowConfig{AutoMerge: true}) assert.Equal(t, 3, table.Length()) assert.Equal(t, 0, len(table.rowsFooterRaw)) assert.Equal(t, 0, len(table.rowsHeaderRaw)) assert.False(t, table.rowsConfigMap[0].AutoMerge) assert.False(t, table.rowsConfigMap[1].AutoMerge) assert.True(t, table.rowsConfigMap[2].AutoMerge) } func TestTable_AppendRows(t *testing.T) { table := Table{} assert.Equal(t, 0, table.Length()) table.AppendRows([]Row{{}}) assert.Equal(t, 1, table.Length()) assert.Equal(t, 0, len(table.rowsFooter)) assert.Equal(t, 0, len(table.rowsHeader)) table.AppendRows([]Row{{}}) assert.Equal(t, 2, table.Length()) assert.Equal(t, 0, len(table.rowsFooter)) assert.Equal(t, 0, len(table.rowsHeader)) table.AppendRows([]Row{{}, {}}, RowConfig{AutoMerge: true}) assert.Equal(t, 4, table.Length()) assert.Equal(t, 0, len(table.rowsFooterRaw)) assert.Equal(t, 0, len(table.rowsHeaderRaw)) assert.False(t, table.rowsConfigMap[0].AutoMerge) assert.False(t, table.rowsConfigMap[1].AutoMerge) assert.True(t, table.rowsConfigMap[2].AutoMerge) assert.True(t, table.rowsConfigMap[3].AutoMerge) } func TestTable_Length(t *testing.T) { table := Table{} assert.Zero(t, table.Length()) table.AppendRow(testRows[0]) assert.Equal(t, 1, table.Length()) table.AppendRow(testRows[1]) assert.Equal(t, 2, table.Length()) table.AppendHeader(testHeader) assert.Equal(t, 2, table.Length()) } func TestTable_ResetFooters(t *testing.T) { table := Table{} table.AppendFooter(testFooter) assert.NotEmpty(t, table.rowsFooterRaw) table.ResetFooters() assert.Empty(t, table.rowsFooterRaw) } func TestTable_ResetHeaders(t *testing.T) { table := Table{} table.AppendHeader(testHeader) assert.NotEmpty(t, table.rowsHeaderRaw) table.ResetHeaders() assert.Empty(t, table.rowsHeaderRaw) } func TestTable_ResetRows(t *testing.T) { table := Table{} table.AppendRows(testRows) assert.NotEmpty(t, table.rowsRaw) table.ResetRows() assert.Empty(t, table.rowsRaw) } func TestTable_SetAllowedRowLength(t *testing.T) { table := Table{} table.AppendRows(testRows) table.SetStyle(styleTest) expectedOutWithNoRowLimit := `(-----^--------^-----------^------^-----------------------------) [< 1>|||<3000>|< >] [< 20>|||<2000>|] [<300>|||<5000>|< >] \-----v--------v-----------v------v-----------------------------/` assert.Zero(t, table.allowedRowLength) assert.Equal(t, expectedOutWithNoRowLimit, table.Render()) table.SetAllowedRowLength(utf8.RuneCountInString(table.style.Box.UnfinishedRow)) assert.Equal(t, utf8.RuneCountInString(table.style.Box.UnfinishedRow), table.allowedRowLength) assert.Equal(t, "", table.Render()) table.SetAllowedRowLength(5) expectedOutWithRowLimit := `( ~~~ [ ~~~ [ ~~~ [ ~~~ \ ~~~` assert.Equal(t, 5, table.allowedRowLength) assert.Equal(t, expectedOutWithRowLimit, table.Render()) table.SetAllowedRowLength(30) expectedOutWithRowLimit = `(-----^--------^---------- ~~~ [< 1>|||||||||<3000>|< >] [< 20>|||<2000>|] [<300>|||<5000>|< >] \-----v--------v-----------v------v-----------------------------/` assert.False(t, table.autoIndex) assert.Equal(t, expectedOut, table.Render()) table.SetAutoIndex(true) expectedOut = `(---^-----^--------^-----------^------^-----------------------------) [< >|< A>|< B >|< C >|< D>|< E >] {---+-----+--------+-----------+------+-----------------------------} [<1>|< 1>|||<3000>|< >] [<2>|< 20>|||<2000>|] [<3>|<300>|||<5000>|< >] \---v-----v--------v-----------v------v-----------------------------/` assert.True(t, table.autoIndex) assert.Equal(t, expectedOut, table.Render()) table.AppendHeader(testHeader) expectedOut = `(---^-----^------------^-----------^--------^-----------------------------) [< >|< #>||||< >] {---+-----+------------+-----------+--------+-----------------------------} [<1>|< 1>|||< 3000>|< >] [<2>|< 20>|||< 2000>|] [<3>|<300>|||< 5000>|< >] \---v-----v------------v-----------v--------v-----------------------------/` assert.True(t, table.autoIndex) assert.Equal(t, expectedOut, table.Render()) table.AppendRow(testRowMultiLine) expectedOut = `(---^-----^------------^-----------^--------^-----------------------------) [< >|< #>||||< >] {---+-----+------------+-----------+--------+-----------------------------} [<1>|< 1>|||< 3000>|< >] [<2>|< 20>|||< 2000>|] [<3>|<300>|||< 5000>|< >] [<4>|< 0>|||< 0>|] [< >|< >|< >|< >|< >|] [< >|< >|< >|< >|< >|] \---v-----v------------v-----------v--------v-----------------------------/` assert.Equal(t, expectedOut, table.Render()) table.SetStyle(StyleLight) expectedOut = `┌───┬─────┬────────────┬───────────┬────────┬─────────────────────────────┐ │ │ # │ FIRST NAME │ LAST NAME │ SALARY │ │ ├───┼─────┼────────────┼───────────┼────────┼─────────────────────────────┤ │ 1 │ 1 │ Arya │ Stark │ 3000 │ │ │ 2 │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │ │ 3 │ 300 │ Tyrion │ Lannister │ 5000 │ │ │ 4 │ 0 │ Winter │ Is │ 0 │ Coming. │ │ │ │ │ │ │ The North Remembers! │ │ │ │ │ │ │ This is known. │ └───┴─────┴────────────┴───────────┴────────┴─────────────────────────────┘` assert.Equal(t, expectedOut, table.Render()) } func TestTable_SetCaption(t *testing.T) { table := Table{} assert.Empty(t, table.caption) table.SetCaption(testCaption) assert.NotEmpty(t, table.caption) assert.Equal(t, testCaption, table.caption) } func TestTable_SetColumnConfigs(t *testing.T) { table := Table{} assert.Empty(t, table.columnConfigs) table.SetColumnConfigs([]ColumnConfig{{}, {}, {}}) assert.NotEmpty(t, table.columnConfigs) assert.Equal(t, 3, len(table.columnConfigs)) } func TestTable_SetHTMLCSSClass(t *testing.T) { table := Table{} table.AppendRow(testRows[0]) expectedHTML := `
          1 Arya Stark 3000
          ` assert.Equal(t, "", table.htmlCSSClass) assert.Equal(t, expectedHTML, table.RenderHTML()) table.SetHTMLCSSClass(testCSSClass) assert.Equal(t, testCSSClass, table.htmlCSSClass) assert.Equal(t, strings.Replace(expectedHTML, DefaultHTMLCSSClass, testCSSClass, -1), table.RenderHTML()) } func TestTable_SetOutputMirror(t *testing.T) { table := Table{} table.AppendRow(testRows[0]) expectedOut := `+---+------+-------+------+ | 1 | Arya | Stark | 3000 | +---+------+-------+------+` assert.Equal(t, nil, table.outputMirror) assert.Equal(t, expectedOut, table.Render()) mockOutputMirror := &myMockOutputMirror{} table.SetOutputMirror(mockOutputMirror) assert.Equal(t, mockOutputMirror, table.outputMirror) assert.Equal(t, expectedOut, table.Render()) assert.Equal(t, expectedOut+"\n", mockOutputMirror.mirroredOutput) } func TestTable_SePageSize(t *testing.T) { table := Table{} assert.Equal(t, 0, table.pageSize) table.SetPageSize(13) assert.Equal(t, 13, table.pageSize) } func TestTable_SortByColumn(t *testing.T) { table := Table{} assert.Empty(t, table.sortBy) table.SortBy([]SortBy{{Name: "#", Mode: Asc}}) assert.Equal(t, 1, len(table.sortBy)) table.SortBy([]SortBy{{Name: "First Name", Mode: Dsc}, {Name: "Last Name", Mode: Asc}}) assert.Equal(t, 2, len(table.sortBy)) } func TestTable_SetStyle(t *testing.T) { table := Table{} assert.NotNil(t, table.Style()) assert.Equal(t, StyleDefault, *table.Style()) table.SetStyle(StyleDefault) assert.NotNil(t, table.Style()) assert.Equal(t, StyleDefault, *table.Style()) } go-pretty-6.2.4/table/util.go000066400000000000000000000020761407250454200160600ustar00rootroot00000000000000package table import ( "reflect" ) // AutoIndexColumnID returns a unique Column ID/Name for the given Column Number. // The functionality is similar to what you get in an Excel spreadsheet w.r.t. // the Column ID/Name. func AutoIndexColumnID(colIdx int) string { charIdx := colIdx % 26 out := string(rune(65 + charIdx)) colIdx = colIdx / 26 if colIdx > 0 { return AutoIndexColumnID(colIdx-1) + out } return out } // isNumber returns true if the argument is a numeric type; false otherwise. func isNumber(x interface{}) bool { switch reflect.TypeOf(x).Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64: return true } return false } // WidthEnforcer is a function that helps enforce a width condition on a string. type WidthEnforcer func(col string, maxLen int) string // widthEnforcerNone returns the input string as is without any modifications. func widthEnforcerNone(col string, maxLen int) string { return col } go-pretty-6.2.4/table/util_test.go000066400000000000000000000033461407250454200171200ustar00rootroot00000000000000package table import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func ExampleAutoIndexColumnID() { fmt.Printf("AutoIndexColumnID( 0): \"%s\"\n", AutoIndexColumnID(0)) fmt.Printf("AutoIndexColumnID( 1): \"%s\"\n", AutoIndexColumnID(1)) fmt.Printf("AutoIndexColumnID( 2): \"%s\"\n", AutoIndexColumnID(2)) fmt.Printf("AutoIndexColumnID( 25): \"%s\"\n", AutoIndexColumnID(25)) fmt.Printf("AutoIndexColumnID( 26): \"%s\"\n", AutoIndexColumnID(26)) fmt.Printf("AutoIndexColumnID( 702): \"%s\"\n", AutoIndexColumnID(702)) fmt.Printf("AutoIndexColumnID(18278): \"%s\"\n", AutoIndexColumnID(18278)) // Output: AutoIndexColumnID( 0): "A" // AutoIndexColumnID( 1): "B" // AutoIndexColumnID( 2): "C" // AutoIndexColumnID( 25): "Z" // AutoIndexColumnID( 26): "AA" // AutoIndexColumnID( 702): "AAA" // AutoIndexColumnID(18278): "AAAA" } func TestAutoIndexColumnID(t *testing.T) { assert.Equal(t, "A", AutoIndexColumnID(0)) assert.Equal(t, "Z", AutoIndexColumnID(25)) assert.Equal(t, "AA", AutoIndexColumnID(26)) assert.Equal(t, "ZZ", AutoIndexColumnID(701)) assert.Equal(t, "AAA", AutoIndexColumnID(702)) assert.Equal(t, "ZZZ", AutoIndexColumnID(18277)) assert.Equal(t, "AAAA", AutoIndexColumnID(18278)) } func TestIsNumber(t *testing.T) { assert.True(t, isNumber(int(1))) assert.True(t, isNumber(int8(1))) assert.True(t, isNumber(int16(1))) assert.True(t, isNumber(int32(1))) assert.True(t, isNumber(int64(1))) assert.True(t, isNumber(uint(1))) assert.True(t, isNumber(uint8(1))) assert.True(t, isNumber(uint16(1))) assert.True(t, isNumber(uint32(1))) assert.True(t, isNumber(uint64(1))) assert.True(t, isNumber(float32(1))) assert.True(t, isNumber(float64(1))) assert.False(t, isNumber("1")) } go-pretty-6.2.4/table/writer.go000066400000000000000000000020461407250454200164140ustar00rootroot00000000000000package table import ( "io" ) // Writer declares the interfaces that can be used to setup and render a table. type Writer interface { AppendFooter(row Row, configs ...RowConfig) AppendHeader(row Row, configs ...RowConfig) AppendRow(row Row, configs ...RowConfig) AppendRows(rows []Row, configs ...RowConfig) AppendSeparator() Length() int Render() string RenderCSV() string RenderHTML() string RenderMarkdown() string ResetFooters() ResetHeaders() ResetRows() SetAllowedRowLength(length int) SetAutoIndex(autoIndex bool) SetCaption(format string, a ...interface{}) SetColumnConfigs(configs []ColumnConfig) SetIndexColumn(colNum int) SetOutputMirror(mirror io.Writer) SetPageSize(numLines int) SetRowPainter(painter RowPainter) SetStyle(style Style) SetTitle(format string, a ...interface{}) SortBy(sortBy []SortBy) Style() *Style SuppressEmptyColumns() // deprecated; in favor of Style().HTML.CSSClass SetHTMLCSSClass(cssClass string) } // NewWriter initializes and returns a Writer. func NewWriter() Writer { return &Table{} } go-pretty-6.2.4/table/writer_test.go000066400000000000000000000074141407250454200174570ustar00rootroot00000000000000package table import ( "fmt" "github.com/jedib0t/go-pretty/v6/text" ) func Example_simple() { // simple table with zero customizations tw := NewWriter() // append a header row tw.AppendHeader(Row{"#", "First Name", "Last Name", "Salary"}) // append some data rows tw.AppendRows([]Row{ {1, "Arya", "Stark", 3000}, {20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"}, {300, "Tyrion", "Lannister", 5000}, }) // append a footer row tw.AppendFooter(Row{"", "", "Total", 10000}) // render it fmt.Printf("Table without any customizations:\n%s", tw.Render()) // Output: Table without any customizations: // +-----+------------+-----------+--------+-----------------------------+ // | # | FIRST NAME | LAST NAME | SALARY | | // +-----+------------+-----------+--------+-----------------------------+ // | 1 | Arya | Stark | 3000 | | // | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | // | 300 | Tyrion | Lannister | 5000 | | // +-----+------------+-----------+--------+-----------------------------+ // | | | TOTAL | 10000 | | // +-----+------------+-----------+--------+-----------------------------+ } func Example_styled() { // table with some amount of customization tw := NewWriter() // append a header row tw.AppendHeader(Row{"First Name", "Last Name", "Salary"}) // append some data rows tw.AppendRows([]Row{ {"Jaime", "Lannister", 5000}, {"Arya", "Stark", 3000, "A girl has no name."}, {"Sansa", "Stark", 4000}, {"Jon", "Snow", 2000, "You know nothing, Jon Snow!"}, {"Tyrion", "Lannister", 5000, "A Lannister always pays his debts."}, }) // append a footer row tw.AppendFooter(Row{"", "Total", 10000}) // auto-index rows tw.SetAutoIndex(true) // sort by last name and then by salary tw.SortBy([]SortBy{{Name: "Last Name", Mode: Dsc}, {Name: "Salary", Mode: AscNumeric}}) // use a ready-to-use style tw.SetStyle(StyleLight) // customize the style and change some stuff tw.Style().Format.Header = text.FormatLower tw.Style().Format.Row = text.FormatLower tw.Style().Format.Footer = text.FormatLower tw.Style().Options.SeparateColumns = false // render it fmt.Printf("Table with customizations:\n%s", tw.Render()) // Output: Table with customizations: // ┌──────────────────────────────────────────────────────────────────────┐ // │ first name last name salary │ // ├──────────────────────────────────────────────────────────────────────┤ // │ 1 arya stark 3000 a girl has no name. │ // │ 2 sansa stark 4000 │ // │ 3 jon snow 2000 you know nothing, jon snow! │ // │ 4 jaime lannister 5000 │ // │ 5 tyrion lannister 5000 a lannister always pays his debts. │ // ├──────────────────────────────────────────────────────────────────────┤ // │ total 10000 │ // └──────────────────────────────────────────────────────────────────────┘ } go-pretty-6.2.4/text/000077500000000000000000000000001407250454200144445ustar00rootroot00000000000000go-pretty-6.2.4/text/align.go000066400000000000000000000075351407250454200160770ustar00rootroot00000000000000package text import ( "fmt" "strconv" "strings" "unicode/utf8" ) // Align denotes how text is to be aligned horizontally. type Align int // Align enumerations const ( AlignDefault Align = iota // same as AlignLeft AlignLeft // "left " AlignCenter // " center " AlignJustify // "justify it" AlignRight // " right" ) // Apply aligns the text as directed. For ex.: // * AlignDefault.Apply("Jon Snow", 12) returns "Jon Snow " // * AlignLeft.Apply("Jon Snow", 12) returns "Jon Snow " // * AlignCenter.Apply("Jon Snow", 12) returns " Jon Snow " // * AlignJustify.Apply("Jon Snow", 12) returns "Jon Snow" // * AlignRight.Apply("Jon Snow", 12) returns " Jon Snow" func (a Align) Apply(text string, maxLength int) string { text = a.trimString(text) sLen := utf8.RuneCountInString(text) sLenWoE := RuneCount(text) numEscChars := sLen - sLenWoE // now, align the text switch a { case AlignDefault, AlignLeft: return fmt.Sprintf("%-"+strconv.Itoa(maxLength+numEscChars)+"s", text) case AlignCenter: if sLenWoE < maxLength { // left pad with half the number of spaces needed before using %text return fmt.Sprintf("%"+strconv.Itoa(maxLength+numEscChars)+"s", text+strings.Repeat(" ", int((maxLength-sLenWoE)/2))) } case AlignJustify: return a.justifyText(text, sLenWoE, maxLength) } return fmt.Sprintf("%"+strconv.Itoa(maxLength+numEscChars)+"s", text) } // HTMLProperty returns the equivalent HTML horizontal-align tag property. func (a Align) HTMLProperty() string { switch a { case AlignLeft: return "align=\"left\"" case AlignCenter: return "align=\"center\"" case AlignJustify: return "align=\"justify\"" case AlignRight: return "align=\"right\"" default: return "" } } // MarkdownProperty returns the equivalent Markdown horizontal-align separator. func (a Align) MarkdownProperty() string { switch a { case AlignLeft: return ":--- " case AlignCenter: return ":---:" case AlignRight: return " ---:" default: return " --- " } } func (a Align) justifyText(text string, textLength int, maxLength int) string { // split the text into individual words wordsUnfiltered := strings.Split(text, " ") words := Filter(wordsUnfiltered, func(item string) bool { return item != "" }) // empty string implies spaces for maxLength if len(words) == 0 { return strings.Repeat(" ", maxLength) } // get the number of spaces to insert into the text numSpacesNeeded := maxLength - textLength + strings.Count(text, " ") numSpacesNeededBetweenWords := 0 if len(words) > 1 { numSpacesNeededBetweenWords = numSpacesNeeded / (len(words) - 1) } // create the output string word by word with spaces in between var outText strings.Builder outText.Grow(maxLength) for idx, word := range words { if idx > 0 { // insert spaces only after the first word if idx == len(words)-1 { // insert all the remaining space before the last word outText.WriteString(strings.Repeat(" ", numSpacesNeeded)) numSpacesNeeded = 0 } else { // insert the determined number of spaces between each word outText.WriteString(strings.Repeat(" ", numSpacesNeededBetweenWords)) // and reduce the number of spaces needed after this numSpacesNeeded -= numSpacesNeededBetweenWords } } outText.WriteString(word) if idx == len(words)-1 && numSpacesNeeded > 0 { outText.WriteString(strings.Repeat(" ", numSpacesNeeded)) } } return outText.String() } func (a Align) trimString(text string) string { switch a { case AlignDefault, AlignLeft: if strings.HasSuffix(text, " ") { return strings.TrimRight(text, " ") } case AlignRight: if strings.HasPrefix(text, " ") { return strings.TrimLeft(text, " ") } default: if strings.HasPrefix(text, " ") || strings.HasSuffix(text, " ") { return strings.Trim(text, " ") } } return text } go-pretty-6.2.4/text/align_test.go000066400000000000000000000136171407250454200171340ustar00rootroot00000000000000package text import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func ExampleAlign_Apply() { fmt.Printf("AlignDefault: '%s'\n", AlignDefault.Apply("Jon Snow", 12)) fmt.Printf("AlignLeft : '%s'\n", AlignLeft.Apply("Jon Snow", 12)) fmt.Printf("AlignCenter : '%s'\n", AlignCenter.Apply("Jon Snow", 12)) fmt.Printf("AlignJustify: '%s'\n", AlignJustify.Apply("Jon Snow", 12)) fmt.Printf("AlignRight : '%s'\n", AlignRight.Apply("Jon Snow", 12)) // Output: AlignDefault: 'Jon Snow ' // AlignLeft : 'Jon Snow ' // AlignCenter : ' Jon Snow ' // AlignJustify: 'Jon Snow' // AlignRight : ' Jon Snow' } func TestAlign_Apply(t *testing.T) { // AlignDefault & AlignLeft are the same assert.Equal(t, "Jon Snow ", AlignDefault.Apply("Jon Snow", 12)) assert.Equal(t, " Jon Snow ", AlignDefault.Apply(" Jon Snow", 12)) assert.Equal(t, " ", AlignDefault.Apply("", 12)) assert.Equal(t, "Jon Snow ", AlignLeft.Apply("Jon Snow ", 12)) assert.Equal(t, " Jon Snow ", AlignLeft.Apply(" Jon Snow ", 12)) assert.Equal(t, " ", AlignLeft.Apply("", 12)) // AlignCenter assert.Equal(t, " Jon Snow ", AlignCenter.Apply("Jon Snow ", 12)) assert.Equal(t, " Jon Snow ", AlignCenter.Apply(" Jon Snow", 12)) assert.Equal(t, " Jon Snow ", AlignCenter.Apply(" Jon Snow ", 12)) assert.Equal(t, " ", AlignCenter.Apply("", 12)) // AlignJustify assert.Equal(t, "Jon Snow", AlignJustify.Apply("Jon Snow", 12)) assert.Equal(t, "JS vs. DT", AlignJustify.Apply("JS vs. DT", 12)) assert.Equal(t, "JS is AT", AlignJustify.Apply("JS is AT", 12)) assert.Equal(t, "JS is AT", AlignJustify.Apply("JS is AT", 12)) assert.Equal(t, "JonSnow ", AlignJustify.Apply("JonSnow", 12)) assert.Equal(t, "JonSnow ", AlignJustify.Apply(" JonSnow", 12)) assert.Equal(t, " ", AlignJustify.Apply("", 12)) // Align Right assert.Equal(t, " Jon Snow", AlignRight.Apply("Jon Snow", 12)) assert.Equal(t, " Jon Snow ", AlignRight.Apply("Jon Snow ", 12)) assert.Equal(t, " Jon Snow ", AlignRight.Apply(" Jon Snow ", 12)) assert.Equal(t, " ", AlignRight.Apply("", 12)) } func TestAlign_Apply_ColoredText(t *testing.T) { // AlignDefault & AlignLeft are the same assert.Equal(t, "\x1b[33mJon Snow\x1b[0m ", AlignDefault.Apply("\x1b[33mJon Snow\x1b[0m", 12)) assert.Equal(t, "\x1b[33m Jon Snow\x1b[0m ", AlignDefault.Apply("\x1b[33m Jon Snow\x1b[0m", 12)) assert.Equal(t, "\x1b[33m\x1b[0m ", AlignDefault.Apply("\x1b[33m\x1b[0m", 12)) assert.Equal(t, "\x1b[33mJon Snow \x1b[0m ", AlignLeft.Apply("\x1b[33mJon Snow \x1b[0m", 12)) assert.Equal(t, "\x1b[33m Jon Snow \x1b[0m ", AlignLeft.Apply("\x1b[33m Jon Snow \x1b[0m", 12)) assert.Equal(t, "\x1b[33m\x1b[0m ", AlignLeft.Apply("\x1b[33m\x1b[0m", 12)) // AlignCenter assert.Equal(t, " \x1b[33mJon Snow \x1b[0m ", AlignCenter.Apply("\x1b[33mJon Snow \x1b[0m", 12)) assert.Equal(t, " \x1b[33m Jon Snow\x1b[0m ", AlignCenter.Apply("\x1b[33m Jon Snow\x1b[0m", 12)) assert.Equal(t, " \x1b[33m Jon Snow \x1b[0m", AlignCenter.Apply("\x1b[33m Jon Snow \x1b[0m", 12)) assert.Equal(t, " \x1b[33m\x1b[0m ", AlignCenter.Apply("\x1b[33m\x1b[0m", 12)) // AlignJustify assert.Equal(t, "\x1b[33mJon Snow\x1b[0m", AlignJustify.Apply("\x1b[33mJon Snow\x1b[0m", 12)) assert.Equal(t, "\x1b[33mJS vs. DT\x1b[0m", AlignJustify.Apply("\x1b[33mJS vs. DT\x1b[0m", 12)) assert.Equal(t, "\x1b[33mJS is AT\x1b[0m", AlignJustify.Apply("\x1b[33mJS is AT\x1b[0m", 12)) assert.Equal(t, "\x1b[33mJS is AT\x1b[0m", AlignJustify.Apply("\x1b[33mJS is AT\x1b[0m", 12)) assert.Equal(t, "\x1b[33mJonSnow\x1b[0m ", AlignJustify.Apply("\x1b[33mJonSnow\x1b[0m", 12)) assert.Equal(t, "\x1b[33m JonSnow\x1b[0m", AlignJustify.Apply("\x1b[33m JonSnow\x1b[0m", 12)) assert.Equal(t, "\x1b[33m\x1b[0m ", AlignJustify.Apply("\x1b[33m\x1b[0m", 12)) // Align Right assert.Equal(t, " \x1b[33mJon Snow\x1b[0m", AlignRight.Apply("\x1b[33mJon Snow\x1b[0m", 12)) assert.Equal(t, " \x1b[33mJon Snow \x1b[0m", AlignRight.Apply("\x1b[33mJon Snow \x1b[0m", 12)) assert.Equal(t, " \x1b[33m Jon Snow \x1b[0m", AlignRight.Apply("\x1b[33m Jon Snow \x1b[0m", 12)) assert.Equal(t, " \x1b[33m\x1b[0m", AlignRight.Apply("\x1b[33m\x1b[0m", 12)) } func ExampleAlign_HTMLProperty() { fmt.Printf("AlignDefault: '%s'\n", AlignDefault.HTMLProperty()) fmt.Printf("AlignLeft : '%s'\n", AlignLeft.HTMLProperty()) fmt.Printf("AlignCenter : '%s'\n", AlignCenter.HTMLProperty()) fmt.Printf("AlignJustify: '%s'\n", AlignJustify.HTMLProperty()) fmt.Printf("AlignRight : '%s'\n", AlignRight.HTMLProperty()) // Output: AlignDefault: '' // AlignLeft : 'align="left"' // AlignCenter : 'align="center"' // AlignJustify: 'align="justify"' // AlignRight : 'align="right"' } func TestAlign_HTMLProperty(t *testing.T) { aligns := map[Align]string{ AlignDefault: "", AlignLeft: "left", AlignCenter: "center", AlignJustify: "justify", AlignRight: "right", } for align, htmlStyle := range aligns { assert.Contains(t, align.HTMLProperty(), htmlStyle) } } func ExampleAlign_MarkdownProperty() { fmt.Printf("AlignDefault: '%s'\n", AlignDefault.MarkdownProperty()) fmt.Printf("AlignLeft : '%s'\n", AlignLeft.MarkdownProperty()) fmt.Printf("AlignCenter : '%s'\n", AlignCenter.MarkdownProperty()) fmt.Printf("AlignJustify: '%s'\n", AlignJustify.MarkdownProperty()) fmt.Printf("AlignRight : '%s'\n", AlignRight.MarkdownProperty()) // Output: AlignDefault: ' --- ' // AlignLeft : ':--- ' // AlignCenter : ':---:' // AlignJustify: ' --- ' // AlignRight : ' ---:' } func TestAlign_MarkdownProperty(t *testing.T) { aligns := map[Align]string{ AlignDefault: " --- ", AlignLeft: ":--- ", AlignCenter: ":---:", AlignJustify: " --- ", AlignRight: " ---:", } for align, markdownSeparator := range aligns { assert.Contains(t, align.MarkdownProperty(), markdownSeparator) } } go-pretty-6.2.4/text/ansi.go000066400000000000000000000034421407250454200157300ustar00rootroot00000000000000package text import "strings" // ANSICodesSupported will be true on consoles where ANSI Escape Codes/Sequences // are supported. var ANSICodesSupported = areANSICodesSupported() // Escape encodes the string with the ANSI Escape Sequence. // For ex.: // Escape("Ghost", "") == "Ghost" // Escape("Ghost", "\x1b[91m") == "\x1b[91mGhost\x1b[0m" // Escape("\x1b[94mGhost\x1b[0mLady", "\x1b[91m") == "\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m" // Escape("Nymeria\x1b[94mGhost\x1b[0mLady", "\x1b[91m") == "\x1b[91mNymeria\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m" // Escape("Nymeria \x1b[94mGhost\x1b[0m Lady", "\x1b[91m") == "\x1b[91mNymeria \x1b[94mGhost\x1b[0m\x1b[91m Lady\x1b[0m" func Escape(str string, escapeSeq string) string { out := "" if !strings.HasPrefix(str, EscapeStart) { out += escapeSeq } out += strings.Replace(str, EscapeReset, EscapeReset+escapeSeq, -1) if !strings.HasSuffix(out, EscapeReset) { out += EscapeReset } if strings.Contains(out, escapeSeq+EscapeReset) { out = strings.Replace(out, escapeSeq+EscapeReset, "", -1) } return out } // StripEscape strips all ANSI Escape Sequence from the string. // For ex.: // StripEscape("Ghost") == "Ghost" // StripEscape("\x1b[91mGhost\x1b[0m") == "Ghost" // StripEscape("\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m") == "GhostLady" // StripEscape("\x1b[91mNymeria\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m") == "NymeriaGhostLady" // StripEscape("\x1b[91mNymeria \x1b[94mGhost\x1b[0m\x1b[91m Lady\x1b[0m") == "Nymeria Ghost Lady" func StripEscape(str string) string { var out strings.Builder out.Grow(RuneCount(str)) isEscSeq := false for _, sChr := range str { if sChr == EscapeStartRune { isEscSeq = true } if !isEscSeq { out.WriteRune(sChr) } if isEscSeq && sChr == EscapeStopRune { isEscSeq = false } } return out.String() } go-pretty-6.2.4/text/ansi_test.go000066400000000000000000000062041407250454200167660ustar00rootroot00000000000000package text import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestEscape(t *testing.T) { assert.Equal(t, "\x1b[91mGhost\x1b[0m", Escape("Ghost", FgHiRed.EscapeSeq())) assert.Equal(t, "\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m", Escape(FgHiBlue.Sprint("Ghost")+"Lady", FgHiRed.EscapeSeq())) assert.Equal(t, "\x1b[91mNymeria\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m", Escape("Nymeria"+FgHiBlue.Sprint("Ghost")+"Lady", FgHiRed.EscapeSeq())) assert.Equal(t, "\x1b[91mNymeria \x1b[94mGhost\x1b[0m\x1b[91m Lady\x1b[0m", Escape("Nymeria "+FgHiBlue.Sprint("Ghost")+" Lady", FgHiRed.EscapeSeq())) } func ExampleEscape() { fmt.Printf("Escape(%#v, %#v) == %#v\n", "Ghost", "", Escape("Ghost", "")) fmt.Printf("Escape(%#v, %#v) == %#v\n", "Ghost", FgHiRed.EscapeSeq(), Escape("Ghost", FgHiRed.EscapeSeq())) fmt.Printf("Escape(%#v, %#v) == %#v\n", FgHiBlue.Sprint("Ghost")+"Lady", FgHiRed.EscapeSeq(), Escape(FgHiBlue.Sprint("Ghost")+"Lady", FgHiRed.EscapeSeq())) fmt.Printf("Escape(%#v, %#v) == %#v\n", "Nymeria"+FgHiBlue.Sprint("Ghost")+"Lady", FgHiRed.EscapeSeq(), Escape("Nymeria"+FgHiBlue.Sprint("Ghost")+"Lady", FgHiRed.EscapeSeq())) fmt.Printf("Escape(%#v, %#v) == %#v\n", "Nymeria "+FgHiBlue.Sprint("Ghost")+" Lady", FgHiRed.EscapeSeq(), Escape("Nymeria "+FgHiBlue.Sprint("Ghost")+" Lady", FgHiRed.EscapeSeq())) // Output: Escape("Ghost", "") == "Ghost" // Escape("Ghost", "\x1b[91m") == "\x1b[91mGhost\x1b[0m" // Escape("\x1b[94mGhost\x1b[0mLady", "\x1b[91m") == "\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m" // Escape("Nymeria\x1b[94mGhost\x1b[0mLady", "\x1b[91m") == "\x1b[91mNymeria\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m" // Escape("Nymeria \x1b[94mGhost\x1b[0m Lady", "\x1b[91m") == "\x1b[91mNymeria \x1b[94mGhost\x1b[0m\x1b[91m Lady\x1b[0m" } func TestStripEscape(t *testing.T) { assert.Equal(t, "Ghost", StripEscape(FgHiRed.Sprint("Ghost"))) assert.Equal(t, "GhostLady", StripEscape(FgHiBlue.Sprint("Ghost")+"Lady")) assert.Equal(t, "NymeriaGhostLady", StripEscape("Nymeria"+FgHiBlue.Sprint("Ghost")+"Lady")) assert.Equal(t, "Nymeria Ghost Lady", StripEscape("Nymeria "+FgHiBlue.Sprint("Ghost")+" Lady")) } func ExampleStripEscape() { fmt.Printf("StripEscape(%#v) == %#v\n", "Ghost", StripEscape("Ghost")) fmt.Printf("StripEscape(%#v) == %#v\n", "\x1b[91mGhost\x1b[0m", StripEscape("\x1b[91mGhost\x1b[0m")) fmt.Printf("StripEscape(%#v) == %#v\n", "\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m", StripEscape("\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m")) fmt.Printf("StripEscape(%#v) == %#v\n", "\x1b[91mNymeria\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m", StripEscape("\x1b[91mNymeria\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m")) fmt.Printf("StripEscape(%#v) == %#v\n", "\x1b[91mNymeria \x1b[94mGhost\x1b[0m\x1b[91m Lady\x1b[0m", StripEscape("\x1b[91mNymeria \x1b[94mGhost\x1b[0m\x1b[91m Lady\x1b[0m")) // Output: StripEscape("Ghost") == "Ghost" // StripEscape("\x1b[91mGhost\x1b[0m") == "Ghost" // StripEscape("\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m") == "GhostLady" // StripEscape("\x1b[91mNymeria\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m") == "NymeriaGhostLady" // StripEscape("\x1b[91mNymeria \x1b[94mGhost\x1b[0m\x1b[91m Lady\x1b[0m") == "Nymeria Ghost Lady" } go-pretty-6.2.4/text/ansi_unix.go000066400000000000000000000001251407250454200167660ustar00rootroot00000000000000// +build !windows package text func areANSICodesSupported() bool { return true } go-pretty-6.2.4/text/ansi_windows.go000066400000000000000000000011111407250454200174710ustar00rootroot00000000000000// +build windows package text import ( "os" "sync" "golang.org/x/sys/windows" ) var ( enableVTPMutex = sync.Mutex{} ) func areANSICodesSupported() bool { enableVTPMutex.Lock() defer enableVTPMutex.Unlock() outHandle := windows.Handle(os.Stdout.Fd()) var outMode uint32 if err := windows.GetConsoleMode(outHandle, &outMode); err == nil { if outMode&windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0 { return true } if err := windows.SetConsoleMode(outHandle, outMode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING); err == nil { return true } } return false } go-pretty-6.2.4/text/color.go000066400000000000000000000074351407250454200161220ustar00rootroot00000000000000package text import ( "fmt" "sort" "strconv" "strings" "sync" ) var ( colorsEnabled = areANSICodesSupported() ) // DisableColors (forcefully) disables color coding globally. func DisableColors() { colorsEnabled = false } // EnableColors (forcefully) enables color coding globally. func EnableColors() { colorsEnabled = true } // The logic here is inspired from github.com/fatih/color; the following is // the the bare minimum logic required to print Colored to the console. // The differences: // * This one caches the escape sequences for cases with multiple colors // * This one handles cases where the incoming already has colors in the // form of escape sequences; in which case, text that does not have any // escape sequences are colored/escaped // Color represents a single color to render with. type Color int // Base colors -- attributes in reality const ( Reset Color = iota Bold Faint Italic Underline BlinkSlow BlinkRapid ReverseVideo Concealed CrossedOut ) // Foreground colors const ( FgBlack Color = iota + 30 FgRed FgGreen FgYellow FgBlue FgMagenta FgCyan FgWhite ) // Foreground Hi-Intensity colors const ( FgHiBlack Color = iota + 90 FgHiRed FgHiGreen FgHiYellow FgHiBlue FgHiMagenta FgHiCyan FgHiWhite ) // Background colors const ( BgBlack Color = iota + 40 BgRed BgGreen BgYellow BgBlue BgMagenta BgCyan BgWhite ) // Background Hi-Intensity colors const ( BgHiBlack Color = iota + 100 BgHiRed BgHiGreen BgHiYellow BgHiBlue BgHiMagenta BgHiCyan BgHiWhite ) // EscapeSeq returns the ANSI escape sequence for the color. func (c Color) EscapeSeq() string { return EscapeStart + strconv.Itoa(int(c)) + EscapeStop } // HTMLProperty returns the "class" attribute for the color. func (c Color) HTMLProperty() string { out := "" if class, ok := colorCSSClassMap[c]; ok { out = fmt.Sprintf("class=\"%s\"", class) } return out } // Sprint colorizes and prints the given string(s). func (c Color) Sprint(a ...interface{}) string { return colorize(fmt.Sprint(a...), c.EscapeSeq()) } // Sprintf formats and colorizes and prints the given string(s). func (c Color) Sprintf(format string, a ...interface{}) string { return colorize(fmt.Sprintf(format, a...), c.EscapeSeq()) } // Colors represents an array of Color objects to render with. // Example: Colors{FgCyan, BgBlack} type Colors []Color var ( // colorsSeqMap caches the escape sequence for a set of colors colorsSeqMap = sync.Map{} ) // EscapeSeq returns the ANSI escape sequence for the colors set. func (c Colors) EscapeSeq() string { if len(c) == 0 { return "" } colorsKey := fmt.Sprintf("%#v", c) escapeSeq, ok := colorsSeqMap.Load(colorsKey) if !ok || escapeSeq == "" { colorNums := make([]string, len(c)) for idx, color := range c { colorNums[idx] = strconv.Itoa(int(color)) } escapeSeq = EscapeStart + strings.Join(colorNums, ";") + EscapeStop colorsSeqMap.Store(colorsKey, escapeSeq) } return escapeSeq.(string) } // HTMLProperty returns the "class" attribute for the colors. func (c Colors) HTMLProperty() string { if len(c) == 0 { return "" } var classes []string for _, color := range c { if class, ok := colorCSSClassMap[color]; ok { classes = append(classes, class) } } if len(classes) > 1 { sort.Strings(classes) } return fmt.Sprintf("class=\"%s\"", strings.Join(classes, " ")) } // Sprint colorizes and prints the given string(s). func (c Colors) Sprint(a ...interface{}) string { return colorize(fmt.Sprint(a...), c.EscapeSeq()) } // Sprintf formats and colorizes and prints the given string(s). func (c Colors) Sprintf(format string, a ...interface{}) string { return colorize(fmt.Sprintf(format, a...), c.EscapeSeq()) } func colorize(s string, escapeSeq string) string { if !colorsEnabled || escapeSeq == "" { return s } return Escape(s, escapeSeq) } go-pretty-6.2.4/text/color_html.go000066400000000000000000000024631407250454200171420ustar00rootroot00000000000000package text var ( // colorCSSClassMap contains the equivalent CSS-class for all colors colorCSSClassMap = map[Color]string{ Bold: "bold", Faint: "faint", Italic: "italic", Underline: "underline", BlinkSlow: "blink-slow", BlinkRapid: "blink-rapid", ReverseVideo: "reverse-video", Concealed: "concealed", CrossedOut: "crossed-out", FgBlack: "fg-black", FgRed: "fg-red", FgGreen: "fg-green", FgYellow: "fg-yellow", FgBlue: "fg-blue", FgMagenta: "fg-magenta", FgCyan: "fg-cyan", FgWhite: "fg-white", FgHiBlack: "fg-hi-black", FgHiRed: "fg-hi-red", FgHiGreen: "fg-hi-green", FgHiYellow: "fg-hi-yellow", FgHiBlue: "fg-hi-blue", FgHiMagenta: "fg-hi-magenta", FgHiCyan: "fg-hi-cyan", FgHiWhite: "fg-hi-white", BgBlack: "bg-black", BgRed: "bg-red", BgGreen: "bg-green", BgYellow: "bg-yellow", BgBlue: "bg-blue", BgMagenta: "bg-magenta", BgCyan: "bg-cyan", BgWhite: "bg-white", BgHiBlack: "bg-hi-black", BgHiRed: "bg-hi-red", BgHiGreen: "bg-hi-green", BgHiYellow: "bg-hi-yellow", BgHiBlue: "bg-hi-blue", BgHiMagenta: "bg-hi-magenta", BgHiCyan: "bg-hi-cyan", BgHiWhite: "bg-hi-white", } ) go-pretty-6.2.4/text/color_test.go000066400000000000000000000136601407250454200171560ustar00rootroot00000000000000package text import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func init() { EnableColors() } func TestColor_EnableAndDisable(t *testing.T) { defer EnableColors() EnableColors() assert.Equal(t, "\x1b[31mtest\x1b[0m", FgRed.Sprint("test")) DisableColors() assert.Equal(t, "test", FgRed.Sprint("test")) EnableColors() assert.Equal(t, "\x1b[31mtest\x1b[0m", FgRed.Sprint("test")) } func ExampleColor_EscapeSeq() { fmt.Printf("Black Background: %#v\n", BgBlack.EscapeSeq()) fmt.Printf("Black Foreground: %#v\n", FgBlack.EscapeSeq()) // Output: Black Background: "\x1b[40m" // Black Foreground: "\x1b[30m" } func TestColor_EscapeSeq(t *testing.T) { assert.Equal(t, "\x1b[40m", BgBlack.EscapeSeq()) } func ExampleColor_HTMLProperty() { fmt.Printf("Bold: %#v\n", Bold.HTMLProperty()) fmt.Printf("Black Background: %#v\n", BgBlack.HTMLProperty()) fmt.Printf("Black Foreground: %#v\n", FgBlack.HTMLProperty()) // Output: Bold: "class=\"bold\"" // Black Background: "class=\"bg-black\"" // Black Foreground: "class=\"fg-black\"" } func TestColor_HTMLProperty(t *testing.T) { assert.Equal(t, "class=\"bold\"", Bold.HTMLProperty()) assert.Equal(t, "class=\"bg-black\"", BgBlack.HTMLProperty()) assert.Equal(t, "class=\"fg-black\"", FgBlack.HTMLProperty()) } func ExampleColor_Sprint() { fmt.Printf("%#v\n", BgBlack.Sprint("Black Background")) fmt.Printf("%#v\n", FgBlack.Sprint("Black Foreground")) // Output: "\x1b[40mBlack Background\x1b[0m" // "\x1b[30mBlack Foreground\x1b[0m" } func TestColor_Sprint(t *testing.T) { assert.Equal(t, "\x1b[31mtest true\x1b[0m", FgRed.Sprint("test ", true)) assert.Equal(t, "\x1b[32mtest\x1b[0m\x1b[31mtrue\x1b[0m", FgRed.Sprint("\x1b[32mtest\x1b[0m", true)) assert.Equal(t, "\x1b[32mtest true\x1b[0m", FgRed.Sprint("\x1b[32mtest ", true)) assert.Equal(t, "\x1b[32mtest\x1b[0m\x1b[31m \x1b[0m", FgRed.Sprint("\x1b[32mtest\x1b[0m ")) assert.Equal(t, "\x1b[32mtest\x1b[0m", FgRed.Sprint("\x1b[32mtest\x1b[0m")) } func ExampleColor_Sprintf() { fmt.Printf("%#v\n", BgBlack.Sprintf("%s %s", "Black", "Background")) fmt.Printf("%#v\n", FgBlack.Sprintf("%s %s", "Black", "Foreground")) // Output: "\x1b[40mBlack Background\x1b[0m" // "\x1b[30mBlack Foreground\x1b[0m" } func TestColor_Sprintf(t *testing.T) { assert.Equal(t, "\x1b[31mtest true\x1b[0m", FgRed.Sprintf("test %s", "true")) } func ExampleColors_EscapeSeq() { fmt.Printf("Black Background: %#v\n", Colors{BgBlack}.EscapeSeq()) fmt.Printf("Black Foreground: %#v\n", Colors{FgBlack}.EscapeSeq()) fmt.Printf("Black Background, White Foreground: %#v\n", Colors{BgBlack, FgWhite}.EscapeSeq()) fmt.Printf("Black Foreground, White Background: %#v\n", Colors{FgBlack, BgWhite}.EscapeSeq()) // Output: Black Background: "\x1b[40m" // Black Foreground: "\x1b[30m" // Black Background, White Foreground: "\x1b[40;37m" // Black Foreground, White Background: "\x1b[30;47m" } func TestColors_EscapeSeq(t *testing.T) { assert.Equal(t, "", Colors{}.EscapeSeq()) assert.Equal(t, "\x1b[40;37m", Colors{BgBlack, FgWhite}.EscapeSeq()) } func ExampleColors_HTMLProperty() { fmt.Printf("Black Background: %#v\n", Colors{BgBlack}.HTMLProperty()) fmt.Printf("Black Foreground: %#v\n", Colors{FgBlack}.HTMLProperty()) fmt.Printf("Black Background, White Foreground: %#v\n", Colors{BgBlack, FgWhite}.HTMLProperty()) fmt.Printf("Black Foreground, White Background: %#v\n", Colors{FgBlack, BgWhite}.HTMLProperty()) fmt.Printf("Bold Italic Underline Red Text: %#v\n", Colors{Bold, Italic, Underline, FgRed}.HTMLProperty()) // Output: Black Background: "class=\"bg-black\"" // Black Foreground: "class=\"fg-black\"" // Black Background, White Foreground: "class=\"bg-black fg-white\"" // Black Foreground, White Background: "class=\"bg-white fg-black\"" // Bold Italic Underline Red Text: "class=\"bold fg-red italic underline\"" } func TestColors_HTMLProperty(t *testing.T) { assert.Equal(t, "", Colors{}.HTMLProperty()) assert.Equal(t, "class=\"bg-black fg-white\"", Colors{BgBlack, FgWhite}.HTMLProperty()) assert.Equal(t, "class=\"bold fg-red\"", Colors{Bold, FgRed}.HTMLProperty()) } func ExampleColors_Sprint() { fmt.Printf("%#v\n", Colors{BgBlack}.Sprint("Black Background")) fmt.Printf("%#v\n", Colors{BgBlack, FgWhite}.Sprint("Black Background, White Foreground")) fmt.Printf("%#v\n", Colors{FgBlack}.Sprint("Black Foreground")) fmt.Printf("%#v\n", Colors{FgBlack, BgWhite}.Sprint("Black Foreground, White Background")) // Output: "\x1b[40mBlack Background\x1b[0m" // "\x1b[40;37mBlack Background, White Foreground\x1b[0m" // "\x1b[30mBlack Foreground\x1b[0m" // "\x1b[30;47mBlack Foreground, White Background\x1b[0m" } func TestColors_Sprint(t *testing.T) { assert.Equal(t, "test true", Colors{}.Sprint("test ", true)) assert.Equal(t, "\x1b[31mtest true\x1b[0m", Colors{FgRed}.Sprint("test ", true)) assert.Equal(t, "\x1b[32mtest\x1b[0m\x1b[31mtrue\x1b[0m", Colors{FgRed}.Sprint("\x1b[32mtest\x1b[0m", true)) assert.Equal(t, "\x1b[32mtest true\x1b[0m", Colors{FgRed}.Sprint("\x1b[32mtest ", true)) assert.Equal(t, "\x1b[32mtest\x1b[0m\x1b[31m \x1b[0m", Colors{FgRed}.Sprint("\x1b[32mtest\x1b[0m ")) assert.Equal(t, "\x1b[32mtest\x1b[0m", Colors{FgRed}.Sprint("\x1b[32mtest\x1b[0m")) } func ExampleColors_Sprintf() { fmt.Printf("%#v\n", Colors{BgBlack}.Sprintf("%s %s", "Black", "Background")) fmt.Printf("%#v\n", Colors{BgBlack, FgWhite}.Sprintf("%s, %s", "Black Background", "White Foreground")) fmt.Printf("%#v\n", Colors{FgBlack}.Sprintf("%s %s", "Black", "Foreground")) fmt.Printf("%#v\n", Colors{FgBlack, BgWhite}.Sprintf("%s, %s", "Black Foreground", "White Background")) // Output: "\x1b[40mBlack Background\x1b[0m" // "\x1b[40;37mBlack Background, White Foreground\x1b[0m" // "\x1b[30mBlack Foreground\x1b[0m" // "\x1b[30;47mBlack Foreground, White Background\x1b[0m" } func TestColors_Sprintf(t *testing.T) { assert.Equal(t, "test true", Colors{}.Sprintf("test %s", "true")) assert.Equal(t, "\x1b[31mtest true\x1b[0m", Colors{FgRed}.Sprintf("test %s", "true")) } go-pretty-6.2.4/text/cursor.go000066400000000000000000000016121407250454200163100ustar00rootroot00000000000000package text import ( "fmt" ) // Cursor helps move the cursor on the console in multiple directions. type Cursor rune const ( // CursorDown helps move the Cursor Down X lines CursorDown Cursor = 'B' // CursorLeft helps move the Cursor Left X characters CursorLeft Cursor = 'D' // CursorRight helps move the Cursor Right X characters CursorRight Cursor = 'C' // CursorUp helps move the Cursor Up X lines CursorUp Cursor = 'A' // EraseLine helps erase all characters to the Right of the Cursor in the // current line EraseLine Cursor = 'K' ) // Sprint prints the Escape Sequence to move the Cursor once. func (c Cursor) Sprint() string { return fmt.Sprintf("%s%c", EscapeStart, c) } // Sprintn prints the Escape Sequence to move the Cursor "n" times. func (c Cursor) Sprintn(n int) string { if c == EraseLine { return c.Sprint() } return fmt.Sprintf("%s%d%c", EscapeStart, n, c) } go-pretty-6.2.4/text/cursor_test.go000066400000000000000000000030321407250454200173450ustar00rootroot00000000000000package text import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func ExampleCursor_Sprint() { fmt.Printf("CursorDown : %#v\n", CursorDown.Sprint()) fmt.Printf("CursorLeft : %#v\n", CursorLeft.Sprint()) fmt.Printf("CursorRight: %#v\n", CursorRight.Sprint()) fmt.Printf("CursorUp : %#v\n", CursorUp.Sprint()) fmt.Printf("EraseLine : %#v\n", EraseLine.Sprint()) // Output: CursorDown : "\x1b[B" // CursorLeft : "\x1b[D" // CursorRight: "\x1b[C" // CursorUp : "\x1b[A" // EraseLine : "\x1b[K" } func TestCursor_Sprint(t *testing.T) { assert.Equal(t, "\x1b[B", CursorDown.Sprint()) assert.Equal(t, "\x1b[D", CursorLeft.Sprint()) assert.Equal(t, "\x1b[C", CursorRight.Sprint()) assert.Equal(t, "\x1b[A", CursorUp.Sprint()) assert.Equal(t, "\x1b[K", EraseLine.Sprint()) } func ExampleCursor_Sprintn() { fmt.Printf("CursorDown : %#v\n", CursorDown.Sprintn(5)) fmt.Printf("CursorLeft : %#v\n", CursorLeft.Sprintn(5)) fmt.Printf("CursorRight: %#v\n", CursorRight.Sprintn(5)) fmt.Printf("CursorUp : %#v\n", CursorUp.Sprintn(5)) fmt.Printf("EraseLine : %#v\n", EraseLine.Sprintn(5)) // Output: CursorDown : "\x1b[5B" // CursorLeft : "\x1b[5D" // CursorRight: "\x1b[5C" // CursorUp : "\x1b[5A" // EraseLine : "\x1b[K" } func TestCursor_Sprintn(t *testing.T) { assert.Equal(t, "\x1b[5B", CursorDown.Sprintn(5)) assert.Equal(t, "\x1b[5D", CursorLeft.Sprintn(5)) assert.Equal(t, "\x1b[5C", CursorRight.Sprintn(5)) assert.Equal(t, "\x1b[5A", CursorUp.Sprintn(5)) assert.Equal(t, "\x1b[K", EraseLine.Sprintn(5)) } go-pretty-6.2.4/text/filter.go000066400000000000000000000004011407250454200162530ustar00rootroot00000000000000package text // Filter filters the slice 's' to items which return truth when passed to 'f'. func Filter(s []string, f func(string) bool) []string { var out []string for _, item := range s { if f(item) { out = append(out, item) } } return out } go-pretty-6.2.4/text/filter_test.go000066400000000000000000000013001407250454200173110ustar00rootroot00000000000000package text import ( "fmt" "strings" "testing" "github.com/stretchr/testify/assert" ) func ExampleFilter() { slice := []string{"Arya Stark", "Bran Stark", "Jon Snow", "Sansa Stark"} filter := func(item string) bool { return strings.HasSuffix(item, "Stark") } fmt.Printf("%#v\n", Filter(slice, filter)) // Output: []string{"Arya Stark", "Bran Stark", "Sansa Stark"} } func TestFilter(t *testing.T) { slice := []string{"Arya Stark", "Bran Stark", "Jon Snow", "Sansa Stark"} filter := func(item string) bool { return strings.HasSuffix(item, "Stark") } filteredSlice := Filter(slice, filter) assert.Equal(t, 3, len(filteredSlice)) assert.NotContains(t, filteredSlice, "Jon Snow") } go-pretty-6.2.4/text/format.go000066400000000000000000000037061407250454200162710ustar00rootroot00000000000000package text import ( "strings" "unicode" ) // Format lets you transform the text in supported methods while keeping escape // sequences in the string intact and untouched. type Format int // Format enumerations const ( FormatDefault Format = iota // default_Case FormatLower // lower FormatTitle // Title FormatUpper // UPPER ) // Apply converts the text as directed. func (tc Format) Apply(text string) string { switch tc { case FormatLower: return strings.ToLower(text) case FormatTitle: return toTitle(text) case FormatUpper: return toUpper(text) default: return text } } func toTitle(text string) string { prev, inEscSeq := ' ', false return strings.Map( func(r rune) rune { if r == EscapeStartRune { inEscSeq = true } if !inEscSeq { if isSeparator(prev) { prev = r r = unicode.ToUpper(r) } else { prev = r } } if inEscSeq && r == EscapeStopRune { inEscSeq = false } return r }, text, ) } func toUpper(text string) string { inEscSeq := false return strings.Map( func(r rune) rune { if r == EscapeStartRune { inEscSeq = true } if !inEscSeq { r = unicode.ToUpper(r) } if inEscSeq && r == EscapeStopRune { inEscSeq = false } return r }, text, ) } // isSeparator returns true if the given rune is a separator. This function is // lifted straight out of the standard library @ strings/strings.go. func isSeparator(r rune) bool { // ASCII alphanumerics and underscore are not separators if r <= 0x7F { switch { case '0' <= r && r <= '9': return false case 'a' <= r && r <= 'z': return false case 'A' <= r && r <= 'Z': return false case r == '_': return false } return true } // Letters and digits are not separators if unicode.IsLetter(r) || unicode.IsDigit(r) { return false } // Otherwise, all we can do for now is treat spaces as separators. return unicode.IsSpace(r) } go-pretty-6.2.4/text/format_test.go000066400000000000000000000040031407250454200173170ustar00rootroot00000000000000package text import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func ExampleFormat_Apply() { fmt.Printf("FormatDefault: %#v\n", FormatDefault.Apply("jon Snow")) fmt.Printf("FormatLower : %#v\n", FormatLower.Apply("jon Snow")) fmt.Printf("FormatTitle : %#v\n", FormatTitle.Apply("jon Snow")) fmt.Printf("FormatUpper : %#v\n", FormatUpper.Apply("jon Snow")) fmt.Println() fmt.Printf("FormatDefault (w/EscSeq): %#v\n", FormatDefault.Apply(Bold.Sprint("jon Snow"))) fmt.Printf("FormatLower (w/EscSeq): %#v\n", FormatLower.Apply(Bold.Sprint("jon Snow"))) fmt.Printf("FormatTitle (w/EscSeq): %#v\n", FormatTitle.Apply(Bold.Sprint("jon Snow"))) fmt.Printf("FormatUpper (w/EscSeq): %#v\n", FormatUpper.Apply(Bold.Sprint("jon Snow"))) // Output: FormatDefault: "jon Snow" // FormatLower : "jon snow" // FormatTitle : "Jon Snow" // FormatUpper : "JON SNOW" // // FormatDefault (w/EscSeq): "\x1b[1mjon Snow\x1b[0m" // FormatLower (w/EscSeq): "\x1b[1mjon snow\x1b[0m" // FormatTitle (w/EscSeq): "\x1b[1mJon Snow\x1b[0m" // FormatUpper (w/EscSeq): "\x1b[1mJON SNOW\x1b[0m" } func TestFormat_Apply(t *testing.T) { text := "A big croc0dile; Died - Empty_fanged ツ \u2008." assert.Equal(t, text, FormatDefault.Apply(text)) assert.Equal(t, "a big croc0dile; died - empty_fanged ツ \u2008.", FormatLower.Apply(text)) assert.Equal(t, "A Big Croc0dile; Died - Empty_fanged ツ \u2008.", FormatTitle.Apply(text)) assert.Equal(t, "A BIG CROC0DILE; DIED - EMPTY_FANGED ツ \u2008.", FormatUpper.Apply(text)) // test with escape sequences text = Colors{Bold}.Sprint(text) assert.Equal(t, "\x1b[1mA big croc0dile; Died - Empty_fanged ツ \u2008.\x1b[0m", FormatDefault.Apply(text)) assert.Equal(t, "\x1b[1ma big croc0dile; died - empty_fanged ツ \u2008.\x1b[0m", FormatLower.Apply(text)) assert.Equal(t, "\x1b[1mA Big Croc0dile; Died - Empty_fanged ツ \u2008.\x1b[0m", FormatTitle.Apply(text)) assert.Equal(t, "\x1b[1mA BIG CROC0DILE; DIED - EMPTY_FANGED ツ \u2008.\x1b[0m", FormatUpper.Apply(text)) } go-pretty-6.2.4/text/string.go000066400000000000000000000123051407250454200163020ustar00rootroot00000000000000package text import ( "strings" "unicode/utf8" "github.com/mattn/go-runewidth" ) // Constants const ( EscapeReset = EscapeStart + "0" + EscapeStop EscapeStart = "\x1b[" EscapeStartRune = rune(27) // \x1b EscapeStop = "m" EscapeStopRune = 'm' ) // InsertEveryN inserts the rune every N characters in the string. For ex.: // InsertEveryN("Ghost", '-', 1) == "G-h-o-s-t" // InsertEveryN("Ghost", '-', 2) == "Gh-os-t" // InsertEveryN("Ghost", '-', 3) == "Gho-st" // InsertEveryN("Ghost", '-', 4) == "Ghos-t" // InsertEveryN("Ghost", '-', 5) == "Ghost" func InsertEveryN(str string, runeToInsert rune, n int) string { if n <= 0 { return str } sLen := RuneCount(str) var out strings.Builder out.Grow(sLen + (sLen / n)) outLen, isEscSeq := 0, false for idx, c := range str { if c == EscapeStartRune { isEscSeq = true } if !isEscSeq && outLen > 0 && (outLen%n) == 0 && idx != sLen { out.WriteRune(runeToInsert) } out.WriteRune(c) if !isEscSeq { outLen += RuneWidth(c) } if isEscSeq && c == EscapeStopRune { isEscSeq = false } } return out.String() } // LongestLineLen returns the length of the longest "line" within the // argument string. For ex.: // LongestLineLen("Ghost!\nCome back here!\nRight now!") == 15 func LongestLineLen(str string) int { maxLength, currLength, isEscSeq := 0, 0, false for _, c := range str { if c == EscapeStartRune { isEscSeq = true } else if isEscSeq && c == EscapeStopRune { isEscSeq = false continue } if c == '\n' { if currLength > maxLength { maxLength = currLength } currLength = 0 } else if !isEscSeq { currLength += RuneWidth(c) } } if currLength > maxLength { maxLength = currLength } return maxLength } // Pad pads the given string with as many characters as needed to make it as // long as specified (maxLen). This function does not count escape sequences // while calculating length of the string. Ex.: // Pad("Ghost", 0, ' ') == "Ghost" // Pad("Ghost", 3, ' ') == "Ghost" // Pad("Ghost", 5, ' ') == "Ghost" // Pad("Ghost", 7, ' ') == "Ghost " // Pad("Ghost", 10, '.') == "Ghost....." func Pad(str string, maxLen int, paddingChar rune) string { strLen := RuneCount(str) if strLen < maxLen { str += strings.Repeat(string(paddingChar), maxLen-strLen) } return str } // RepeatAndTrim repeats the given string until it is as long as maxRunes. // For ex.: // RepeatAndTrim("", 5) == "" // RepeatAndTrim("Ghost", 0) == "" // RepeatAndTrim("Ghost", 5) == "Ghost" // RepeatAndTrim("Ghost", 7) == "GhostGh" // RepeatAndTrim("Ghost", 10) == "GhostGhost" func RepeatAndTrim(str string, maxRunes int) string { if str == "" || maxRunes == 0 { return "" } else if maxRunes == utf8.RuneCountInString(str) { return str } repeatedS := strings.Repeat(str, int(maxRunes/utf8.RuneCountInString(str))+1) return Trim(repeatedS, maxRunes) } // RuneCount is similar to utf8.RuneCountInString, except for the fact that it // ignores escape sequences while counting. For ex.: // RuneCount("") == 0 // RuneCount("Ghost") == 5 // RuneCount("\x1b[33mGhost\x1b[0m") == 5 // RuneCount("\x1b[33mGhost\x1b[0") == 5 func RuneCount(str string) int { count, isEscSeq := 0, false for _, c := range str { if c == EscapeStartRune { isEscSeq = true } else if isEscSeq { if c == EscapeStopRune { isEscSeq = false } } else { count += RuneWidth(c) } } return count } // RuneWidth returns the mostly accurate character-width of the rune. This is // not 100% accurate as the character width is usually dependant on the // typeface (font) used in the console/terminal. For ex.: // RuneWidth('A') == 1 // RuneWidth('ツ') == 2 // RuneWidth('⊙') == 1 // RuneWidth('︿') == 2 // RuneWidth(0x27) == 0 func RuneWidth(r rune) int { return runewidth.RuneWidth(r) } // Snip returns the given string with a fixed length. For ex.: // Snip("Ghost", 0, "~") == "Ghost" // Snip("Ghost", 1, "~") == "~" // Snip("Ghost", 3, "~") == "Gh~" // Snip("Ghost", 5, "~") == "Ghost" // Snip("Ghost", 7, "~") == "Ghost " // Snip("\x1b[33mGhost\x1b[0m", 7, "~") == "\x1b[33mGhost\x1b[0m " func Snip(str string, length int, snipIndicator string) string { if length > 0 { lenStr := RuneCount(str) if lenStr > length { lenStrFinal := length - RuneCount(snipIndicator) return Trim(str, lenStrFinal) + snipIndicator } } return str } // Trim trims a string to the given length while ignoring escape sequences. For // ex.: // Trim("Ghost", 3) == "Gho" // Trim("Ghost", 6) == "Ghost" // Trim("\x1b[33mGhost\x1b[0m", 3) == "\x1b[33mGho\x1b[0m" // Trim("\x1b[33mGhost\x1b[0m", 6) == "\x1b[33mGhost\x1b[0m" func Trim(str string, maxLen int) string { if maxLen <= 0 { return "" } var out strings.Builder out.Grow(maxLen) outLen, isEscSeq, lastEscSeq := 0, false, strings.Builder{} for _, sChr := range str { out.WriteRune(sChr) if sChr == EscapeStartRune { isEscSeq = true lastEscSeq.Reset() lastEscSeq.WriteRune(sChr) } else if isEscSeq { lastEscSeq.WriteRune(sChr) if sChr == EscapeStopRune { isEscSeq = false } } else { outLen++ if outLen == maxLen { break } } } if lastEscSeq.Len() > 0 && lastEscSeq.String() != EscapeReset { out.WriteString(EscapeReset) } return out.String() } go-pretty-6.2.4/text/string_test.go000066400000000000000000000235341407250454200173470ustar00rootroot00000000000000package text import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func ExampleInsertEveryN() { fmt.Printf("InsertEveryN(\"Ghost\", '-', 0): %#v\n", InsertEveryN("Ghost", '-', 0)) fmt.Printf("InsertEveryN(\"Ghost\", '-', 1): %#v\n", InsertEveryN("Ghost", '-', 1)) fmt.Printf("InsertEveryN(\"Ghost\", '-', 2): %#v\n", InsertEveryN("Ghost", '-', 2)) fmt.Printf("InsertEveryN(\"Ghost\", '-', 3): %#v\n", InsertEveryN("Ghost", '-', 3)) fmt.Printf("InsertEveryN(\"Ghost\", '-', 4): %#v\n", InsertEveryN("Ghost", '-', 4)) fmt.Printf("InsertEveryN(\"Ghost\", '-', 5): %#v\n", InsertEveryN("Ghost", '-', 5)) fmt.Printf("InsertEveryN(\"\\x1b[33mGhost\\x1b[0m\", '-', 0): %#v\n", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 0)) fmt.Printf("InsertEveryN(\"\\x1b[33mGhost\\x1b[0m\", '-', 1): %#v\n", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 1)) fmt.Printf("InsertEveryN(\"\\x1b[33mGhost\\x1b[0m\", '-', 2): %#v\n", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 2)) fmt.Printf("InsertEveryN(\"\\x1b[33mGhost\\x1b[0m\", '-', 3): %#v\n", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 3)) fmt.Printf("InsertEveryN(\"\\x1b[33mGhost\\x1b[0m\", '-', 4): %#v\n", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 4)) fmt.Printf("InsertEveryN(\"\\x1b[33mGhost\\x1b[0m\", '-', 5): %#v\n", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 5)) // Output: InsertEveryN("Ghost", '-', 0): "Ghost" // InsertEveryN("Ghost", '-', 1): "G-h-o-s-t" // InsertEveryN("Ghost", '-', 2): "Gh-os-t" // InsertEveryN("Ghost", '-', 3): "Gho-st" // InsertEveryN("Ghost", '-', 4): "Ghos-t" // InsertEveryN("Ghost", '-', 5): "Ghost" // InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 0): "\x1b[33mGhost\x1b[0m" // InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 1): "\x1b[33mG-h-o-s-t\x1b[0m" // InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 2): "\x1b[33mGh-os-t\x1b[0m" // InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 3): "\x1b[33mGho-st\x1b[0m" // InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 4): "\x1b[33mGhos-t\x1b[0m" // InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 5): "\x1b[33mGhost\x1b[0m" } func TestInsertEveryN(t *testing.T) { assert.Equal(t, "Ghost", InsertEveryN("Ghost", '-', 0)) assert.Equal(t, "Gツhツoツsツt", InsertEveryN("Ghost", 'ツ', 1)) assert.Equal(t, "G-h-o-s-t", InsertEveryN("Ghost", '-', 1)) assert.Equal(t, "Gh-os-t", InsertEveryN("Ghost", '-', 2)) assert.Equal(t, "Gho-st", InsertEveryN("Ghost", '-', 3)) assert.Equal(t, "Ghos-t", InsertEveryN("Ghost", '-', 4)) assert.Equal(t, "Ghost", InsertEveryN("Ghost", '-', 5)) assert.Equal(t, "\x1b[33mGhost\x1b[0m", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 0)) assert.Equal(t, "\x1b[33mGツhツoツsツt\x1b[0m", InsertEveryN("\x1b[33mGhost\x1b[0m", 'ツ', 1)) assert.Equal(t, "\x1b[33mG-h-o-s-t\x1b[0m", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 1)) assert.Equal(t, "\x1b[33mGh-os-t\x1b[0m", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 2)) assert.Equal(t, "\x1b[33mGho-st\x1b[0m", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 3)) assert.Equal(t, "\x1b[33mGhos-t\x1b[0m", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 4)) assert.Equal(t, "\x1b[33mGhost\x1b[0m", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 5)) } func ExampleLongestLineLen() { fmt.Printf("LongestLineLen(\"\"): %d\n", LongestLineLen("")) fmt.Printf("LongestLineLen(\"\\n\\n\"): %d\n", LongestLineLen("\n\n")) fmt.Printf("LongestLineLen(\"Ghost\"): %d\n", LongestLineLen("Ghost")) fmt.Printf("LongestLineLen(\"Ghostツ\"): %d\n", LongestLineLen("Ghostツ")) fmt.Printf("LongestLineLen(\"Winter\\nIs\\nComing\"): %d\n", LongestLineLen("Winter\nIs\nComing")) fmt.Printf("LongestLineLen(\"Mother\\nOf\\nDragons\"): %d\n", LongestLineLen("Mother\nOf\nDragons")) fmt.Printf("LongestLineLen(\"\\x1b[33mMother\\x1b[0m\\nOf\\nDragons\"): %d\n", LongestLineLen("\x1b[33mMother\x1b[0m\nOf\nDragons")) // Output: LongestLineLen(""): 0 // LongestLineLen("\n\n"): 0 // LongestLineLen("Ghost"): 5 // LongestLineLen("Ghostツ"): 7 // LongestLineLen("Winter\nIs\nComing"): 6 // LongestLineLen("Mother\nOf\nDragons"): 7 // LongestLineLen("\x1b[33mMother\x1b[0m\nOf\nDragons"): 7 } func TestLongestLineLen(t *testing.T) { assert.Equal(t, 0, LongestLineLen("")) assert.Equal(t, 0, LongestLineLen("\n\n")) assert.Equal(t, 5, LongestLineLen("Ghost")) assert.Equal(t, 7, LongestLineLen("Ghostツ")) assert.Equal(t, 6, LongestLineLen("Winter\nIs\nComing")) assert.Equal(t, 7, LongestLineLen("Mother\nOf\nDragons")) assert.Equal(t, 7, LongestLineLen("\x1b[33mMother\x1b[0m\nOf\nDragons")) } func ExamplePad() { fmt.Printf("%#v\n", Pad("Ghost", 0, ' ')) fmt.Printf("%#v\n", Pad("Ghost", 3, ' ')) fmt.Printf("%#v\n", Pad("Ghost", 5, ' ')) fmt.Printf("%#v\n", Pad("\x1b[33mGhost\x1b[0m", 7, '-')) fmt.Printf("%#v\n", Pad("\x1b[33mGhost\x1b[0m", 10, '.')) // Output: "Ghost" // "Ghost" // "Ghost" // "\x1b[33mGhost\x1b[0m--" // "\x1b[33mGhost\x1b[0m....." } func TestPad(t *testing.T) { assert.Equal(t, "Ghost", Pad("Ghost", 0, ' ')) assert.Equal(t, "Ghost", Pad("Ghost", 3, ' ')) assert.Equal(t, "Ghost", Pad("Ghost", 5, ' ')) assert.Equal(t, "Ghost ", Pad("Ghost", 7, ' ')) assert.Equal(t, "Ghost.....", Pad("Ghost", 10, '.')) assert.Equal(t, "\x1b[33mGhost\x1b[0 ", Pad("\x1b[33mGhost\x1b[0", 7, ' ')) assert.Equal(t, "\x1b[33mGhost\x1b[0.....", Pad("\x1b[33mGhost\x1b[0", 10, '.')) } func ExampleRepeatAndTrim() { fmt.Printf("RepeatAndTrim(\"\", 5): %#v\n", RepeatAndTrim("", 5)) fmt.Printf("RepeatAndTrim(\"Ghost\", 0): %#v\n", RepeatAndTrim("Ghost", 0)) fmt.Printf("RepeatAndTrim(\"Ghost\", 3): %#v\n", RepeatAndTrim("Ghost", 3)) fmt.Printf("RepeatAndTrim(\"Ghost\", 5): %#v\n", RepeatAndTrim("Ghost", 5)) fmt.Printf("RepeatAndTrim(\"Ghost\", 7): %#v\n", RepeatAndTrim("Ghost", 7)) fmt.Printf("RepeatAndTrim(\"Ghost\", 10): %#v\n", RepeatAndTrim("Ghost", 10)) // Output: RepeatAndTrim("", 5): "" // RepeatAndTrim("Ghost", 0): "" // RepeatAndTrim("Ghost", 3): "Gho" // RepeatAndTrim("Ghost", 5): "Ghost" // RepeatAndTrim("Ghost", 7): "GhostGh" // RepeatAndTrim("Ghost", 10): "GhostGhost" } func TestRepeatAndTrim(t *testing.T) { assert.Equal(t, "", RepeatAndTrim("", 5)) assert.Equal(t, "", RepeatAndTrim("Ghost", 0)) assert.Equal(t, "Gho", RepeatAndTrim("Ghost", 3)) assert.Equal(t, "Ghost", RepeatAndTrim("Ghost", 5)) assert.Equal(t, "GhostGh", RepeatAndTrim("Ghost", 7)) assert.Equal(t, "GhostGhost", RepeatAndTrim("Ghost", 10)) assert.Equal(t, "───", RepeatAndTrim("─", 3)) } func ExampleRuneCount() { fmt.Printf("RuneCount(\"\"): %d\n", RuneCount("")) fmt.Printf("RuneCount(\"Ghost\"): %d\n", RuneCount("Ghost")) fmt.Printf("RuneCount(\"Ghostツ\"): %d\n", RuneCount("Ghostツ")) fmt.Printf("RuneCount(\"\\x1b[33mGhost\\x1b[0m\"): %d\n", RuneCount("\x1b[33mGhost\x1b[0m")) fmt.Printf("RuneCount(\"\\x1b[33mGhost\\x1b[0\"): %d\n", RuneCount("\x1b[33mGhost\x1b[0")) // Output: RuneCount(""): 0 // RuneCount("Ghost"): 5 // RuneCount("Ghostツ"): 7 // RuneCount("\x1b[33mGhost\x1b[0m"): 5 // RuneCount("\x1b[33mGhost\x1b[0"): 5 } func TestRuneCount(t *testing.T) { assert.Equal(t, 0, RuneCount("")) assert.Equal(t, 5, RuneCount("Ghost")) assert.Equal(t, 7, RuneCount("Ghostツ")) assert.Equal(t, 5, RuneCount("\x1b[33mGhost\x1b[0m")) assert.Equal(t, 5, RuneCount("\x1b[33mGhost\x1b[0")) } func ExampleRuneWidth() { fmt.Printf("RuneWidth('A'): %d\n", RuneWidth('A')) fmt.Printf("RuneWidth('ツ'): %d\n", RuneWidth('ツ')) fmt.Printf("RuneWidth('⊙'): %d\n", RuneWidth('⊙')) fmt.Printf("RuneWidth('︿'): %d\n", RuneWidth('︿')) fmt.Printf("RuneWidth(rune(27)): %d\n", RuneWidth(rune(27))) // ANSI escape sequence // Output: RuneWidth('A'): 1 // RuneWidth('ツ'): 2 // RuneWidth('⊙'): 1 // RuneWidth('︿'): 2 // RuneWidth(rune(27)): 0 } func TestRuneWidth(t *testing.T) { assert.Equal(t, 1, RuneWidth('A')) assert.Equal(t, 2, RuneWidth('ツ')) assert.Equal(t, 1, RuneWidth('⊙')) assert.Equal(t, 2, RuneWidth('︿')) assert.Equal(t, 0, RuneWidth(rune(27))) // ANSI escape sequence } func ExampleSnip() { fmt.Printf("Snip(\"Ghost\", 0, \"~\"): %#v\n", Snip("Ghost", 0, "~")) fmt.Printf("Snip(\"Ghost\", 1, \"~\"): %#v\n", Snip("Ghost", 1, "~")) fmt.Printf("Snip(\"Ghost\", 3, \"~\"): %#v\n", Snip("Ghost", 3, "~")) fmt.Printf("Snip(\"Ghost\", 5, \"~\"): %#v\n", Snip("Ghost", 5, "~")) fmt.Printf("Snip(\"Ghost\", 7, \"~\"): %#v\n", Snip("Ghost", 7, "~")) fmt.Printf("Snip(\"\\x1b[33mGhost\\x1b[0m\", 7, \"~\"): %#v\n", Snip("\x1b[33mGhost\x1b[0m", 7, "~")) // Output: Snip("Ghost", 0, "~"): "Ghost" // Snip("Ghost", 1, "~"): "~" // Snip("Ghost", 3, "~"): "Gh~" // Snip("Ghost", 5, "~"): "Ghost" // Snip("Ghost", 7, "~"): "Ghost" // Snip("\x1b[33mGhost\x1b[0m", 7, "~"): "\x1b[33mGhost\x1b[0m" } func TestSnip(t *testing.T) { assert.Equal(t, "Ghost", Snip("Ghost", 0, "~")) assert.Equal(t, "~", Snip("Ghost", 1, "~")) assert.Equal(t, "Gh~", Snip("Ghost", 3, "~")) assert.Equal(t, "Ghost", Snip("Ghost", 5, "~")) assert.Equal(t, "Ghost", Snip("Ghost", 7, "~")) assert.Equal(t, "\x1b[33mGhost\x1b[0m", Snip("\x1b[33mGhost\x1b[0m", 7, "~")) } func ExampleTrim() { fmt.Printf("Trim(\"Ghost\", 0): %#v\n", Trim("Ghost", 0)) fmt.Printf("Trim(\"Ghost\", 3): %#v\n", Trim("Ghost", 3)) fmt.Printf("Trim(\"Ghost\", 6): %#v\n", Trim("Ghost", 6)) fmt.Printf("Trim(\"\\x1b[33mGhost\\x1b[0m\", 0): %#v\n", Trim("\x1b[33mGhost\x1b[0m", 0)) fmt.Printf("Trim(\"\\x1b[33mGhost\\x1b[0m\", 3): %#v\n", Trim("\x1b[33mGhost\x1b[0m", 3)) fmt.Printf("Trim(\"\\x1b[33mGhost\\x1b[0m\", 6): %#v\n", Trim("\x1b[33mGhost\x1b[0m", 6)) // Output: Trim("Ghost", 0): "" // Trim("Ghost", 3): "Gho" // Trim("Ghost", 6): "Ghost" // Trim("\x1b[33mGhost\x1b[0m", 0): "" // Trim("\x1b[33mGhost\x1b[0m", 3): "\x1b[33mGho\x1b[0m" // Trim("\x1b[33mGhost\x1b[0m", 6): "\x1b[33mGhost\x1b[0m" } func TestTrim(t *testing.T) { assert.Equal(t, "", Trim("Ghost", 0)) assert.Equal(t, "Gho", Trim("Ghost", 3)) assert.Equal(t, "Ghost", Trim("Ghost", 6)) assert.Equal(t, "\x1b[33mGho\x1b[0m", Trim("\x1b[33mGhost\x1b[0m", 3)) assert.Equal(t, "\x1b[33mGhost\x1b[0m", Trim("\x1b[33mGhost\x1b[0m", 6)) } go-pretty-6.2.4/text/transformer.go000066400000000000000000000136461407250454200173470ustar00rootroot00000000000000package text import ( "bytes" "encoding/json" "fmt" "strconv" "strings" "time" ) // Transformer related constants const ( unixTimeMinMilliseconds = int64(10000000000) unixTimeMinMicroseconds = unixTimeMinMilliseconds * 1000 unixTimeMinNanoSeconds = unixTimeMinMicroseconds * 1000 ) // Transformer related variables var ( colorsNumberPositive = Colors{FgHiGreen} colorsNumberNegative = Colors{FgHiRed} colorsNumberZero = Colors{} colorsURL = Colors{Underline, FgBlue} rfc3339Milli = "2006-01-02T15:04:05.000Z07:00" rfc3339Micro = "2006-01-02T15:04:05.000000Z07:00" possibleTimeLayouts = []string{ time.RFC3339, rfc3339Milli, // strfmt.DateTime.String()'s default layout rfc3339Micro, time.RFC3339Nano, } ) // Transformer helps format the contents of an object to the user's liking. type Transformer func(val interface{}) string // NewNumberTransformer returns a number Transformer that: // * transforms the number as directed by 'format' (ex.: %.2f) // * colors negative values Red // * colors positive values Green func NewNumberTransformer(format string) Transformer { return func(val interface{}) string { if number, ok := val.(int); ok { return transformInt(format, int64(number)) } if number, ok := val.(int8); ok { return transformInt(format, int64(number)) } if number, ok := val.(int16); ok { return transformInt(format, int64(number)) } if number, ok := val.(int32); ok { return transformInt(format, int64(number)) } if number, ok := val.(int64); ok { return transformInt(format, int64(number)) } if number, ok := val.(uint); ok { return transformUint(format, uint64(number)) } if number, ok := val.(uint8); ok { return transformUint(format, uint64(number)) } if number, ok := val.(uint16); ok { return transformUint(format, uint64(number)) } if number, ok := val.(uint32); ok { return transformUint(format, uint64(number)) } if number, ok := val.(uint64); ok { return transformUint(format, uint64(number)) } if number, ok := val.(float32); ok { return transformFloat(format, float64(number)) } if number, ok := val.(float64); ok { return transformFloat(format, float64(number)) } return fmt.Sprint(val) } } func transformInt(format string, val int64) string { if val < 0 { return colorsNumberNegative.Sprintf("-"+format, -val) } if val > 0 { return colorsNumberPositive.Sprintf(format, val) } return colorsNumberZero.Sprintf(format, val) } func transformUint(format string, val uint64) string { if val > 0 { return colorsNumberPositive.Sprintf(format, val) } return colorsNumberZero.Sprintf(format, val) } func transformFloat(format string, val float64) string { if val < 0 { return colorsNumberNegative.Sprintf("-"+format, -val) } if val > 0 { return colorsNumberPositive.Sprintf(format, val) } return colorsNumberZero.Sprintf(format, val) } // NewJSONTransformer returns a Transformer that can format a JSON string or an // object into pretty-indented JSON-strings. func NewJSONTransformer(prefix string, indent string) Transformer { return func(val interface{}) string { if valStr, ok := val.(string); ok { var b bytes.Buffer if err := json.Indent(&b, []byte(strings.TrimSpace(valStr)), prefix, indent); err == nil { return string(b.Bytes()) } } else if b, err := json.MarshalIndent(val, prefix, indent); err == nil { return string(b) } return fmt.Sprintf("%#v", val) } } // NewTimeTransformer returns a Transformer that can format a timestamp (a // time.Time) into a well-defined time format defined using the provided layout // (ex.: time.RFC3339). // // If a non-nil location value is provided, the time will be localized to that // location (use time.Local to get localized timestamps). func NewTimeTransformer(layout string, location *time.Location) Transformer { return func(val interface{}) string { formatTime := func(t time.Time) string { rsp := "" if t.Unix() > 0 { if location != nil { t = t.In(location) } rsp = t.Format(layout) } return rsp } rsp := fmt.Sprint(val) if valTime, ok := val.(time.Time); ok { rsp = formatTime(valTime) } else { // cycle through some supported layouts to see if the string form // of the object matches any of these layouts for _, possibleTimeLayout := range possibleTimeLayouts { if valTime, err := time.Parse(possibleTimeLayout, rsp); err == nil { rsp = formatTime(valTime) break } } } return rsp } } // NewUnixTimeTransformer returns a Transformer that can format a unix-timestamp // into a well-defined time format as defined by 'layout'. This can handle // unix-time in Seconds, MilliSeconds, Microseconds and Nanoseconds. // // If a non-nil location value is provided, the time will be localized to that // location (use time.Local to get localized timestamps). func NewUnixTimeTransformer(layout string, location *time.Location) Transformer { timeTransformer := NewTimeTransformer(layout, location) formatUnixTime := func(unixTime int64) string { if unixTime >= unixTimeMinNanoSeconds { unixTime = unixTime / time.Second.Nanoseconds() } else if unixTime >= unixTimeMinMicroseconds { unixTime = unixTime / (time.Second.Nanoseconds() / 1000) } else if unixTime >= unixTimeMinMilliseconds { unixTime = unixTime / (time.Second.Nanoseconds() / 1000000) } return timeTransformer(time.Unix(unixTime, 0)) } return func(val interface{}) string { if unixTime, ok := val.(int64); ok { return formatUnixTime(unixTime) } else if unixTimeStr, ok := val.(string); ok { if unixTime, err := strconv.ParseInt(unixTimeStr, 10, 64); err == nil { return formatUnixTime(unixTime) } } return fmt.Sprint(val) } } // NewURLTransformer returns a Transformer that can format and pretty print a string // that contains an URL (the text is underlined and colored Blue). func NewURLTransformer() Transformer { return func(val interface{}) string { return colorsURL.Sprint(val) } } go-pretty-6.2.4/text/transformer_test.go000066400000000000000000000150011407250454200203710ustar00rootroot00000000000000package text import ( "fmt" "reflect" "strings" "testing" "time" "github.com/stretchr/testify/assert" ) func TestNewNumberTransformer(t *testing.T) { signColorsMap := map[string]Colors{ "negative": colorsNumberNegative, "positive": colorsNumberPositive, "zero": nil, } colorValuesMap := map[string]map[interface{}]string{ "negative": { int(-5): "%05d", int8(-5): "%05d", int16(-5): "%05d", int32(-5): "%05d", int64(-5): "%05d", float32(-5.55555): "%08.2f", float64(-5.55555): "%08.2f", }, "positive": { int(5): "%05d", int8(5): "%05d", int16(5): "%05d", int32(5): "%05d", int64(5): "%05d", uint(5): "%05d", uint8(5): "%05d", uint16(5): "%05d", uint32(5): "%05d", uint64(5): "%05d", float32(5.55555): "%08.2f", float64(5.55555): "%08.2f", }, "zero": { int(0): "%05d", int8(0): "%05d", int16(0): "%05d", int32(0): "%05d", int64(0): "%05d", uint(0): "%05d", uint8(0): "%05d", uint16(0): "%05d", uint32(0): "%05d", uint64(0): "%05d", float32(0.00000): "%08.2f", float64(0.00000): "%08.2f", }, } for sign, valuesFormatMap := range colorValuesMap { for value, format := range valuesFormatMap { transformer := NewNumberTransformer(format) expected := signColorsMap[sign].Sprintf(format, value) if sign == "negative" { expected = strings.Replace(expected, "-0", "-00", 1) } actual := transformer(value) message := fmt.Sprintf("%s.%s: expected=%v, actual=%v; format=%#v", sign, reflect.TypeOf(value).Kind(), expected, actual, format) assert.Equal(t, expected, actual, message) } } // invalid input assert.Equal(t, "foo", NewNumberTransformer("%05d")("foo")) } type jsonTest struct { Foo string `json:"foo"` Bar int32 `json:"bar"` Baz float64 `json:"baz"` Nan jsonNestTest `json:"nan"` } type jsonNestTest struct { A string B int32 C float64 } func TestNewJSONTransformer(t *testing.T) { transformer := NewJSONTransformer("", " ") // instance of a struct inputObj := jsonTest{ Foo: "fooooooo", Bar: 13, Baz: 3.14, Nan: jsonNestTest{ A: "a", B: 2, C: 3.0, }, } expectedOutput := `{ "foo": "fooooooo", "bar": 13, "baz": 3.14, "nan": { "A": "a", "B": 2, "C": 3 } }` assert.Equal(t, expectedOutput, transformer(inputObj)) // numbers assert.Equal(t, "1", transformer(int(1))) assert.Equal(t, "1.2345", transformer(float32(1.2345))) // slices assert.Equal(t, "[\n 1,\n 2,\n 3\n]", transformer([]uint{1, 2, 3})) // strings assert.Equal(t, "\"foo\"", transformer("foo")) assert.Equal(t, "\"{foo...\"", transformer("{foo...")) // malformed JSON // strings with valid JSON input := "{\"foo\":\"bar\",\"baz\":[1,2,3]}" expectedOutput = `{ "foo": "bar", "baz": [ 1, 2, 3 ] }` assert.Equal(t, expectedOutput, transformer(input)) } func TestNewTimeTransformer(t *testing.T) { inStr := "2010-11-12T13:14:15-07:00" inTime, err := time.Parse(time.RFC3339, inStr) assert.Nil(t, err) location, err := time.LoadLocation("America/Los_Angeles") assert.Nil(t, err) transformer := NewTimeTransformer(time.RFC3339, location) expected := "2010-11-12T12:14:15-08:00" assert.Equal(t, expected, transformer(inStr)) assert.Equal(t, expected, transformer(inTime)) for _, possibleTimeLayout := range possibleTimeLayouts { assert.Equal(t, expected, transformer(inTime.Format(possibleTimeLayout)), possibleTimeLayout) } location, err = time.LoadLocation("Asia/Singapore") assert.Nil(t, err) transformer = NewTimeTransformer(time.UnixDate, location) expected = "Sat Nov 13 04:14:15 +08 2010" assert.Equal(t, expected, transformer(inStr)) assert.Equal(t, expected, transformer(inTime)) for _, possibleTimeLayout := range possibleTimeLayouts { assert.Equal(t, expected, transformer(inTime.Format(possibleTimeLayout)), possibleTimeLayout) } location, err = time.LoadLocation("Europe/London") assert.Nil(t, err) transformer = NewTimeTransformer(time.RFC3339, location) expected = "2010-11-12T20:14:15Z" assert.Equal(t, expected, transformer(inStr)) assert.Equal(t, expected, transformer(inTime)) for _, possibleTimeLayout := range possibleTimeLayouts { assert.Equal(t, expected, transformer(inTime.Format(possibleTimeLayout)), possibleTimeLayout) } } func TestNewUnixTimeTransformer(t *testing.T) { inStr := "2010-11-12T13:14:15-07:00" inTime, err := time.Parse(time.RFC3339, inStr) assert.Nil(t, err) inUnixTime := inTime.Unix() location, err := time.LoadLocation("America/Los_Angeles") assert.Nil(t, err) transformer := NewUnixTimeTransformer(time.RFC3339, location) expected := "2010-11-12T12:14:15-08:00" assert.Equal(t, expected, transformer(fmt.Sprint(inUnixTime)), "seconds in string") assert.Equal(t, expected, transformer(inUnixTime), "seconds") assert.Equal(t, expected, transformer(inUnixTime*1000), "milliseconds") assert.Equal(t, expected, transformer(inUnixTime*1000000), "microseconds") assert.Equal(t, expected, transformer(inUnixTime*1000000000), "nanoseconds") location, err = time.LoadLocation("Asia/Singapore") assert.Nil(t, err) transformer = NewUnixTimeTransformer(time.UnixDate, location) expected = "Sat Nov 13 04:14:15 +08 2010" assert.Equal(t, expected, transformer(fmt.Sprint(inUnixTime)), "seconds in string") assert.Equal(t, expected, transformer(inUnixTime), "seconds") assert.Equal(t, expected, transformer(inUnixTime*1000), "milliseconds") assert.Equal(t, expected, transformer(inUnixTime*1000000), "microseconds") assert.Equal(t, expected, transformer(inUnixTime*1000000000), "nanoseconds") location, err = time.LoadLocation("Europe/London") assert.Nil(t, err) transformer = NewUnixTimeTransformer(time.RFC3339, location) expected = "2010-11-12T20:14:15Z" assert.Equal(t, expected, transformer(fmt.Sprint(inUnixTime)), "seconds in string") assert.Equal(t, expected, transformer(inUnixTime), "seconds") assert.Equal(t, expected, transformer(inUnixTime*1000), "milliseconds") assert.Equal(t, expected, transformer(inUnixTime*1000000), "microseconds") assert.Equal(t, expected, transformer(inUnixTime*1000000000), "nanoseconds") assert.Equal(t, "0.123456", transformer(float32(0.123456))) } func TestNewURLTransformer(t *testing.T) { url := "https://winter.is.coming" transformer := NewURLTransformer() assert.Equal(t, colorsURL.Sprint(url), transformer(url)) } go-pretty-6.2.4/text/valign.go000066400000000000000000000036471407250454200162650ustar00rootroot00000000000000package text import "strings" // VAlign denotes how text is to be aligned vertically. type VAlign int // VAlign enumerations const ( VAlignDefault VAlign = iota // same as VAlignTop VAlignTop // "top\n\n" VAlignMiddle // "\nmiddle\n" VAlignBottom // "\n\nbottom" ) // Apply aligns the lines vertically. For ex.: // * VAlignTop.Apply({"Game", "Of", "Thrones"}, 5) // returns {"Game", "Of", "Thrones", "", ""} // * VAlignMiddle.Apply({"Game", "Of", "Thrones"}, 5) // returns {"", "Game", "Of", "Thrones", ""} // * VAlignBottom.Apply({"Game", "Of", "Thrones"}, 5) // returns {"", "", "Game", "Of", "Thrones"} func (va VAlign) Apply(lines []string, maxLines int) []string { if len(lines) == maxLines { return lines } else if len(lines) > maxLines { maxLines = len(lines) } insertIdx := 0 if va == VAlignMiddle { insertIdx = int(maxLines-len(lines)) / 2 } else if va == VAlignBottom { insertIdx = maxLines - len(lines) } linesOut := strings.Split(strings.Repeat("\n", maxLines-1), "\n") for idx, line := range lines { linesOut[idx+insertIdx] = line } return linesOut } // ApplyStr aligns the string (of 1 or more lines) vertically. For ex.: // * VAlignTop.ApplyStr("Game\nOf\nThrones", 5) // returns {"Game", "Of", "Thrones", "", ""} // * VAlignMiddle.ApplyStr("Game\nOf\nThrones", 5) // returns {"", "Game", "Of", "Thrones", ""} // * VAlignBottom.ApplyStr("Game\nOf\nThrones", 5) // returns {"", "", "Game", "Of", "Thrones"} func (va VAlign) ApplyStr(text string, maxLines int) []string { return va.Apply(strings.Split(text, "\n"), maxLines) } // HTMLProperty returns the equivalent HTML vertical-align tag property. func (va VAlign) HTMLProperty() string { switch va { case VAlignTop: return "valign=\"top\"" case VAlignMiddle: return "valign=\"middle\"" case VAlignBottom: return "valign=\"bottom\"" default: return "" } } go-pretty-6.2.4/text/valign_test.go000066400000000000000000000075621407250454200173240ustar00rootroot00000000000000package text import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) func ExampleVAlign_Apply() { lines := []string{"Game", "Of", "Thrones"} maxLines := 5 fmt.Printf("VAlignDefault: %#v\n", VAlignDefault.Apply(lines, maxLines)) fmt.Printf("VAlignTop : %#v\n", VAlignTop.Apply(lines, maxLines)) fmt.Printf("VAlignMiddle : %#v\n", VAlignMiddle.Apply(lines, maxLines)) fmt.Printf("VAlignBottom : %#v\n", VAlignBottom.Apply(lines, maxLines)) // Output: VAlignDefault: []string{"Game", "Of", "Thrones", "", ""} // VAlignTop : []string{"Game", "Of", "Thrones", "", ""} // VAlignMiddle : []string{"", "Game", "Of", "Thrones", ""} // VAlignBottom : []string{"", "", "Game", "Of", "Thrones"} } func TestVAlign_Apply(t *testing.T) { assert.Equal(t, []string{"Game", "Of", "Thrones"}, VAlignDefault.Apply([]string{"Game", "Of", "Thrones"}, 1)) assert.Equal(t, []string{"Game", "Of", "Thrones"}, VAlignDefault.Apply([]string{"Game", "Of", "Thrones"}, 3)) assert.Equal(t, []string{"Game", "Of", "Thrones", "", ""}, VAlignDefault.Apply([]string{"Game", "Of", "Thrones"}, 5)) assert.Equal(t, []string{"Game", "Of", "Thrones"}, VAlignTop.Apply([]string{"Game", "Of", "Thrones"}, 1)) assert.Equal(t, []string{"Game", "Of", "Thrones"}, VAlignTop.Apply([]string{"Game", "Of", "Thrones"}, 3)) assert.Equal(t, []string{"Game", "Of", "Thrones", "", ""}, VAlignTop.Apply([]string{"Game", "Of", "Thrones"}, 5)) assert.Equal(t, []string{"Game", "Of", "Thrones"}, VAlignMiddle.Apply([]string{"Game", "Of", "Thrones"}, 1)) assert.Equal(t, []string{"Game", "Of", "Thrones"}, VAlignMiddle.Apply([]string{"Game", "Of", "Thrones"}, 3)) assert.Equal(t, []string{"", "Game", "Of", "Thrones", ""}, VAlignMiddle.Apply([]string{"Game", "Of", "Thrones"}, 5)) assert.Equal(t, []string{"Game", "Of", "Thrones"}, VAlignBottom.Apply([]string{"Game", "Of", "Thrones"}, 1)) assert.Equal(t, []string{"Game", "Of", "Thrones"}, VAlignBottom.Apply([]string{"Game", "Of", "Thrones"}, 3)) assert.Equal(t, []string{"", "", "Game", "Of", "Thrones"}, VAlignBottom.Apply([]string{"Game", "Of", "Thrones"}, 5)) } func ExampleVAlign_ApplyStr() { str := "Game\nOf\nThrones" maxLines := 5 fmt.Printf("VAlignDefault: %#v\n", VAlignDefault.ApplyStr(str, maxLines)) fmt.Printf("VAlignTop : %#v\n", VAlignTop.ApplyStr(str, maxLines)) fmt.Printf("VAlignMiddle : %#v\n", VAlignMiddle.ApplyStr(str, maxLines)) fmt.Printf("VAlignBottom : %#v\n", VAlignBottom.ApplyStr(str, maxLines)) // Output: VAlignDefault: []string{"Game", "Of", "Thrones", "", ""} // VAlignTop : []string{"Game", "Of", "Thrones", "", ""} // VAlignMiddle : []string{"", "Game", "Of", "Thrones", ""} // VAlignBottom : []string{"", "", "Game", "Of", "Thrones"} } func TestVAlign_ApplyStr(t *testing.T) { assert.Equal(t, []string{"Game", "Of", "Thrones", "", ""}, VAlignDefault.ApplyStr("Game\nOf\nThrones", 5)) assert.Equal(t, []string{"Game", "Of", "Thrones", "", ""}, VAlignTop.ApplyStr("Game\nOf\nThrones", 5)) assert.Equal(t, []string{"", "Game", "Of", "Thrones", ""}, VAlignMiddle.ApplyStr("Game\nOf\nThrones", 5)) assert.Equal(t, []string{"", "", "Game", "Of", "Thrones"}, VAlignBottom.ApplyStr("Game\nOf\nThrones", 5)) } func ExampleVAlign_HTMLProperty() { fmt.Printf("VAlignDefault: '%s'\n", VAlignDefault.HTMLProperty()) fmt.Printf("VAlignTop : '%s'\n", VAlignTop.HTMLProperty()) fmt.Printf("VAlignMiddle : '%s'\n", VAlignMiddle.HTMLProperty()) fmt.Printf("VAlignBottom : '%s'\n", VAlignBottom.HTMLProperty()) // Output: VAlignDefault: '' // VAlignTop : 'valign="top"' // VAlignMiddle : 'valign="middle"' // VAlignBottom : 'valign="bottom"' } func TestVAlign_HTMLProperty(t *testing.T) { vAligns := map[VAlign]string{ VAlignDefault: "", VAlignTop: "top", VAlignMiddle: "middle", VAlignBottom: "bottom", } for vAlign, htmlStyle := range vAligns { assert.Contains(t, vAlign.HTMLProperty(), htmlStyle) } } go-pretty-6.2.4/text/wrap.go000066400000000000000000000150121407250454200157430ustar00rootroot00000000000000package text import ( "strings" "unicode/utf8" ) // WrapHard wraps a string to the given length using a newline. Handles strings // with ANSI escape sequences (such as text color) without breaking the text // formatting. Breaks all words that go beyond the line boundary. // // For examples, refer to the unit-tests or GoDoc examples. func WrapHard(str string, wrapLen int) string { if wrapLen <= 0 { return "" } str = strings.Replace(str, "\t", " ", -1) sLen := utf8.RuneCountInString(str) if sLen <= wrapLen { return str } out := &strings.Builder{} out.Grow(sLen + (sLen / wrapLen)) for idx, paragraph := range strings.Split(str, "\n\n") { if idx > 0 { out.WriteString("\n\n") } wrapHard(paragraph, wrapLen, out) } return out.String() } // WrapSoft wraps a string to the given length using a newline. Handles strings // with ANSI escape sequences (such as text color) without breaking the text // formatting. Tries to move words that go beyond the line boundary to the next // line. // // For examples, refer to the unit-tests or GoDoc examples. func WrapSoft(str string, wrapLen int) string { if wrapLen <= 0 { return "" } str = strings.Replace(str, "\t", " ", -1) sLen := utf8.RuneCountInString(str) if sLen <= wrapLen { return str } out := &strings.Builder{} out.Grow(sLen + (sLen / wrapLen)) for idx, paragraph := range strings.Split(str, "\n\n") { if idx > 0 { out.WriteString("\n\n") } wrapSoft(paragraph, wrapLen, out) } return out.String() } // WrapText is very similar to WrapHard except for one minor difference. Unlike // WrapHard which discards line-breaks and respects only paragraph-breaks, this // function respects line-breaks too. // // For examples, refer to the unit-tests or GoDoc examples. func WrapText(str string, wrapLen int) string { if wrapLen <= 0 { return "" } var out strings.Builder sLen := utf8.RuneCountInString(str) out.Grow(sLen + (sLen / wrapLen)) lineIdx, isEscSeq, lastEscSeq := 0, false, "" for _, char := range str { if char == EscapeStartRune { isEscSeq = true lastEscSeq = "" } if isEscSeq { lastEscSeq += string(char) } appendChar(char, wrapLen, &lineIdx, isEscSeq, lastEscSeq, &out) if isEscSeq && char == EscapeStopRune { isEscSeq = false } if lastEscSeq == EscapeReset { lastEscSeq = "" } } if lastEscSeq != "" && lastEscSeq != EscapeReset { out.WriteString(EscapeReset) } return out.String() } func appendChar(char rune, wrapLen int, lineLen *int, inEscSeq bool, lastSeenEscSeq string, out *strings.Builder) { // handle reaching the end of the line as dictated by wrapLen or by finding // a newline character if (*lineLen == wrapLen && !inEscSeq && char != '\n') || (char == '\n') { if lastSeenEscSeq != "" { // terminate escape sequence and the line; and restart the escape // sequence in the next line out.WriteString(EscapeReset) out.WriteRune('\n') out.WriteString(lastSeenEscSeq) } else { // just start a new line out.WriteRune('\n') } // reset line index to 0th character *lineLen = 0 } // if the rune is not a new line, output it if char != '\n' { out.WriteRune(char) // increment the line index if not in the middle of an escape sequence if !inEscSeq { *lineLen++ } } } func appendWord(word string, lineIdx *int, lastSeenEscSeq string, wrapLen int, out *strings.Builder) { inEscSeq := false for _, char := range word { if char == EscapeStartRune { inEscSeq = true lastSeenEscSeq = "" } if inEscSeq { lastSeenEscSeq += string(char) } appendChar(char, wrapLen, lineIdx, inEscSeq, lastSeenEscSeq, out) if inEscSeq && char == EscapeStopRune { inEscSeq = false } if lastSeenEscSeq == EscapeReset { lastSeenEscSeq = "" } } } func extractOpenEscapeSeq(str string) string { escapeSeq, inEscSeq := "", false for _, char := range str { if char == EscapeStartRune { inEscSeq = true escapeSeq = "" } if inEscSeq { escapeSeq += string(char) } if char == EscapeStopRune { inEscSeq = false } } if escapeSeq == EscapeReset { escapeSeq = "" } return escapeSeq } func terminateLine(wrapLen int, lineLen *int, lastSeenEscSeq string, out *strings.Builder) { if *lineLen < wrapLen { out.WriteString(strings.Repeat(" ", wrapLen-*lineLen)) } // something is already on the line; terminate it if lastSeenEscSeq != "" { out.WriteString(EscapeReset) } out.WriteRune('\n') out.WriteString(lastSeenEscSeq) *lineLen = 0 } func terminateOutput(lastSeenEscSeq string, out *strings.Builder) { if lastSeenEscSeq != "" && lastSeenEscSeq != EscapeReset && !strings.HasSuffix(out.String(), EscapeReset) { out.WriteString(EscapeReset) } } func wrapHard(paragraph string, wrapLen int, out *strings.Builder) { lineLen, lastSeenEscSeq := 0, "" words := strings.Fields(paragraph) for wordIdx, word := range words { escSeq := extractOpenEscapeSeq(word) if escSeq != "" { lastSeenEscSeq = escSeq } if lineLen > 0 { out.WriteRune(' ') lineLen++ } wordLen := RuneCount(word) if lineLen+wordLen <= wrapLen { // word fits within the line out.WriteString(word) lineLen += wordLen } else { // word doesn't fit within the line; hard-wrap appendWord(word, &lineLen, lastSeenEscSeq, wrapLen, out) } // end of line; but more words incoming if lineLen == wrapLen && wordIdx < len(words)-1 { terminateLine(wrapLen, &lineLen, lastSeenEscSeq, out) } } terminateOutput(lastSeenEscSeq, out) } func wrapSoft(paragraph string, wrapLen int, out *strings.Builder) { lineLen, lastSeenEscSeq := 0, "" words := strings.Fields(paragraph) for wordIdx, word := range words { escSeq := extractOpenEscapeSeq(word) if escSeq != "" { lastSeenEscSeq = escSeq } spacing, spacingLen := "", 0 if lineLen > 0 { spacing, spacingLen = " ", 1 } wordLen := RuneCount(word) if lineLen+spacingLen+wordLen <= wrapLen { // word fits within the line out.WriteString(spacing) out.WriteString(word) lineLen += spacingLen + wordLen } else { // word doesn't fit within the line if lineLen > 0 { // something is already on the line; terminate it terminateLine(wrapLen, &lineLen, lastSeenEscSeq, out) } if wordLen <= wrapLen { // word fits within a single line out.WriteString(word) lineLen = wordLen } else { // word doesn't fit within a single line; hard-wrap appendWord(word, &lineLen, lastSeenEscSeq, wrapLen, out) } } // end of line; but more words incoming if lineLen == wrapLen && wordIdx < len(words)-1 { terminateLine(wrapLen, &lineLen, lastSeenEscSeq, out) } } terminateOutput(lastSeenEscSeq, out) } go-pretty-6.2.4/text/wrap_test.go000066400000000000000000000151761407250454200170150ustar00rootroot00000000000000package text import ( "fmt" "strings" "testing" "github.com/stretchr/testify/assert" ) func ExampleWrapHard() { str := `The quick brown fox jumped over the lazy dog. A big crocodile died empty-fanged, gulping horribly in jerking kicking little motions. Nonchalant old Peter Quinn ruthlessly shot the under-water vermin with Xavier yelling Zap!` strWrapped := WrapHard(str, 30) for idx, line := range strings.Split(strWrapped, "\n") { fmt.Printf("Line #%02d: '%s'\n", idx+1, line) } // Output: Line #01: 'The quick brown fox jumped ove' // Line #02: 'r the lazy dog.' // Line #03: '' // Line #04: 'A big crocodile died empty-fan' // Line #05: 'ged, gulping horribly in jerki' // Line #06: 'ng kicking little motions. Non' // Line #07: 'chalant old Peter Quinn ruthle' // Line #08: 'ssly shot the under-water verm' // Line #09: 'in with Xavier yelling Zap!' } func TestWrapHard(t *testing.T) { assert.Equal(t, "", WrapHard("Ghost", 0)) assert.Equal(t, "G\nh\no\ns\nt", WrapHard("Ghost", 1)) assert.Equal(t, "Gh\nos\nt", WrapHard("Ghost", 2)) assert.Equal(t, "Gho\nst", WrapHard("Ghost", 3)) assert.Equal(t, "Ghos\nt", WrapHard("Ghost", 4)) assert.Equal(t, "Ghost", WrapHard("Ghost", 5)) assert.Equal(t, "Ghost", WrapHard("Ghost", 6)) assert.Equal(t, "Jo\nn \nSn\now", WrapHard("Jon\nSnow", 2)) assert.Equal(t, "Jo\nn \nSn\now", WrapHard("Jon\nSnow\n", 2)) assert.Equal(t, "Jon\nSno\nw", WrapHard("Jon\nSnow\n", 3)) assert.Equal(t, "Jon i\ns a S\nnow", WrapHard("Jon is a Snow", 5)) assert.Equal(t, "\x1b[33mJon\x1b[0m\nSno\nw", WrapHard("\x1b[33mJon\x1b[0m\nSnow", 3)) assert.Equal(t, "\x1b[33mJon\x1b[0m\nSno\nw", WrapHard("\x1b[33mJon\x1b[0m\nSnow\n", 3)) assert.Equal(t, "\x1b[33mJon\x1b[0m\n\x1b[33mSno\x1b[0m\n\x1b[33mw\x1b[0m", WrapHard("\x1b[33mJon Snow\x1b[0m", 3)) assert.Equal(t, "\x1b[33mJon\x1b[0m\n\x1b[33mSno\x1b[0m\n\x1b[33mw\x1b[0m", WrapHard("\x1b[33mJon Snow\n", 3)) assert.Equal(t, "\x1b[33mJon\x1b[0m\n\x1b[33mSno\x1b[0m\n\x1b[33mw \x1b[0m", WrapHard("\x1b[33mJon Snow\n\x1b[0m", 3)) complexIn := "+---+------+-------+------+\n| 1 | Arya | Stark | 3000 |\n+---+------+-------+------+" assert.Equal(t, complexIn, WrapHard(complexIn, 27)) } func ExampleWrapSoft() { str := `The quick brown fox jumped over the lazy dog. A big crocodile died empty-fanged, gulping horribly in jerking kicking little motions. Nonchalant old Peter Quinn ruthlessly shot the under-water vermin with Xavier yelling Zap!` strWrapped := WrapSoft(str, 30) for idx, line := range strings.Split(strWrapped, "\n") { fmt.Printf("Line #%02d: '%s'\n", idx+1, line) } // Output: Line #01: 'The quick brown fox jumped ' // Line #02: 'over the lazy dog.' // Line #03: '' // Line #04: 'A big crocodile died ' // Line #05: 'empty-fanged, gulping horribly' // Line #06: 'in jerking kicking little ' // Line #07: 'motions. Nonchalant old Peter ' // Line #08: 'Quinn ruthlessly shot the ' // Line #09: 'under-water vermin with Xavier' // Line #10: 'yelling Zap!' } func TestWrapSoft(t *testing.T) { assert.Equal(t, "", WrapSoft("Ghost", 0)) assert.Equal(t, "G\nh\no\ns\nt", WrapSoft("Ghost", 1)) assert.Equal(t, "Gh\nos\nt", WrapSoft("Ghost", 2)) assert.Equal(t, "Gho\nst", WrapSoft("Ghost", 3)) assert.Equal(t, "Ghos\nt", WrapSoft("Ghost", 4)) assert.Equal(t, "Ghost", WrapSoft("Ghost", 5)) assert.Equal(t, "Ghost", WrapSoft("Ghost", 6)) assert.Equal(t, "Jo\nn \nSn\now", WrapSoft("Jon\nSnow", 2)) assert.Equal(t, "Jo\nn \nSn\now", WrapSoft("Jon\nSnow\n", 2)) assert.Equal(t, "Jon\nSno\nw", WrapSoft("Jon\nSnow\n", 3)) assert.Equal(t, "Jon \nSnow", WrapSoft("Jon\nSnow", 4)) assert.Equal(t, "Jon \nSnow", WrapSoft("Jon\nSnow\n", 4)) assert.Equal(t, "Jon \nis a \nSnow", WrapSoft("Jon is a Snow", 5)) assert.Equal(t, "\x1b[33mJon\x1b[0m\nSno\nw", WrapSoft("\x1b[33mJon\x1b[0m\nSnow", 3)) assert.Equal(t, "\x1b[33mJon\x1b[0m\nSno\nw", WrapSoft("\x1b[33mJon\x1b[0m\nSnow\n", 3)) assert.Equal(t, "\x1b[33mJon\x1b[0m\n\x1b[33mSno\x1b[0m\n\x1b[33mw\x1b[0m", WrapSoft("\x1b[33mJon Snow\x1b[0m", 3)) assert.Equal(t, "\x1b[33mJon\x1b[0m\n\x1b[33mSno\x1b[0m\n\x1b[33mw\x1b[0m", WrapSoft("\x1b[33mJon Snow\n", 3)) assert.Equal(t, "\x1b[33mJon\x1b[0m\n\x1b[33mSno\x1b[0m\n\x1b[33mw \x1b[0m", WrapSoft("\x1b[33mJon Snow\n\x1b[0m", 3)) complexIn := "+---+------+-------+------+\n| 1 | Arya | Stark | 3000 |\n+---+------+-------+------+" assert.Equal(t, complexIn, WrapSoft(complexIn, 27)) assert.Equal(t, "\x1b[33mJon \x1b[0m\n\x1b[33mSnow\x1b[0m", WrapSoft("\x1b[33mJon Snow\x1b[0m", 4)) assert.Equal(t, "\x1b[33mJon \x1b[0m\n\x1b[33mSnow\x1b[0m\n\x1b[33m???\x1b[0m", WrapSoft("\x1b[33mJon Snow???\x1b[0m", 4)) } func ExampleWrapText() { str := `The quick brown fox jumped over the lazy dog. A big crocodile died empty-fanged, gulping horribly in jerking kicking little motions. Nonchalant old Peter Quinn ruthlessly shot the under-water vermin with Xavier yelling Zap!` strWrapped := WrapText(str, 30) for idx, line := range strings.Split(strWrapped, "\n") { fmt.Printf("Line #%02d: '%s'\n", idx+1, line) } // Output: Line #01: 'The quick brown fox jumped ove' // Line #02: 'r the lazy dog.' // Line #03: '' // Line #04: 'A big crocodile died empty-fan' // Line #05: 'ged, gulping horribly in jerki' // Line #06: 'ng kicking little' // Line #07: 'motions. Nonchalant old Peter ' // Line #08: 'Quinn ruthlessly shot the unde' // Line #09: 'r-water vermin with' // Line #10: 'Xavier yelling Zap!' } func TestWrapText(t *testing.T) { assert.Equal(t, "", WrapText("Ghost", 0)) assert.Equal(t, "G\nh\no\ns\nt", WrapText("Ghost", 1)) assert.Equal(t, "Gh\nos\nt", WrapText("Ghost", 2)) assert.Equal(t, "Gho\nst", WrapText("Ghost", 3)) assert.Equal(t, "Ghos\nt", WrapText("Ghost", 4)) assert.Equal(t, "Ghost", WrapText("Ghost", 5)) assert.Equal(t, "Ghost", WrapText("Ghost", 6)) assert.Equal(t, "Jo\nn\nSn\now", WrapText("Jon\nSnow", 2)) assert.Equal(t, "Jo\nn\nSn\now\n", WrapText("Jon\nSnow\n", 2)) assert.Equal(t, "Jon\nSno\nw\n", WrapText("Jon\nSnow\n", 3)) assert.Equal(t, "\x1b[33mJon\x1b[0m\nSno\nw", WrapText("\x1b[33mJon\x1b[0m\nSnow", 3)) assert.Equal(t, "\x1b[33mJon\x1b[0m\nSno\nw\n", WrapText("\x1b[33mJon\x1b[0m\nSnow\n", 3)) assert.Equal(t, "\x1b[33mJon\x1b[0m\n\x1b[33m Sn\x1b[0m\n\x1b[33mow\x1b[0m", WrapText("\x1b[33mJon Snow\x1b[0m", 3)) assert.Equal(t, "\x1b[33mJon\x1b[0m\n\x1b[33m Sn\x1b[0m\n\x1b[33mow\x1b[0m\n\x1b[33m\x1b[0m", WrapText("\x1b[33mJon Snow\n", 3)) assert.Equal(t, "\x1b[33mJon\x1b[0m\n\x1b[33m Sn\x1b[0m\n\x1b[33mow\x1b[0m\n\x1b[33m\x1b[0m", WrapText("\x1b[33mJon Snow\n\x1b[0m", 3)) complexIn := "+---+------+-------+------+\n| 1 | Arya | Stark | 3000 |\n+---+------+-------+------+" assert.Equal(t, complexIn, WrapText(complexIn, 27)) }