pax_global_header00006660000000000000000000000064145416057070014523gustar00rootroot0000000000000052 comment=335ac2eb445a052d2c63500cf5d07462d5602987 stable-0.1.7/000077500000000000000000000000001454160570700130025ustar00rootroot00000000000000stable-0.1.7/.gitignore000066400000000000000000000000131454160570700147640ustar00rootroot00000000000000.directory stable-0.1.7/CHANGELOG.md000066400000000000000000000012121454160570700146070ustar00rootroot00000000000000# Changelog - v0.1.7 - 2023-11-23 - if the perameter bufRows in method Writer is 0, all data will be kept in memory - v0.1.6 - 2023-11-23 - set the upbound of the number of buffer rows as 1 million. - v0.1.5 - 2023-11-24 - replace tabs in cells with spaces. - v0.1.4 - 2023-08-18 - added a new style: StyleThreeLine (tree-line table). - v0.1.3 - 2023-08-18 - do not set hasHeader with true if all headers are empty strings. - added a new method: HasHeaders. - v0.1.2 - 2023-06-27 - fix setting a global MaxWidth short than cell texts. - v0.1.1 - 2023-06-27 - fix go.mod file - v0.1.0 - 2023-06-27 - first version stable-0.1.7/LICENSE000077500000000000000000000021041454160570700140070ustar00rootroot00000000000000Copyright (c) 2023 Wei Shen (shenwei356@gmail.com) The MIT License 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. stable-0.1.7/README.md000077500000000000000000000516751454160570700143020ustar00rootroot00000000000000# stable - streaming pretty text table [![Go Reference](https://pkg.go.dev/badge/github.com/shenwei356/stable.svg)](https://pkg.go.dev/github.com/shenwei356/stable) If you just need an executable tool to format tables, please use `csvtk pretty` ([code](https://github.com/shenwei356/csvtk), [usage and example](https://bioinf.shenwei.me/csvtk/usage/#pretty)), which uses this package. Table of Contents * [Features](#features) * [Install](#install) * [Examples](#examples) * [Styles](#styles) * [Support](#support) * [License](#license) * [Alternate packages](#alternate-packages) ## Features - **Supporting streaming output** (optional). When a writer is configured, a newly added row is formatted and written to the writer immediately. It is memory-effective for a large number of rows. And it is helpful to pipe the data in shell. - **Supporting wrapping text or clipping text**. The minimum and maximum width of the column can be configured for each column or globally. - **Configured table styles**. Some [preset styles](#styles) are also provided. - **Unicode supported** Not-supported features: - Row/column span - Colorful text ## Install go get -u github.com/shenwei356/table ## Examples **Note that the output is well-formatted in the terminal. However, rows containing Unicode are not displayed appropriately in text editors and browsers.** 1. Basic usages. tbl := New().HumanizeNumbers().MaxWidth(40) tbl.Header([]string{ "id", "name", "sentence", }) tbl.AddRow([]interface{}{100, "Donec Vitae", "Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse."}) tbl.AddRow([]interface{}{2000, "Quaerat Voluptatem", "At vero eos et accusamus et iusto odio."}) tbl.AddRow([]interface{}{3000000, "Aliquam lorem", "Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero."}) fmt.Printf("%s\n", tbl.Render(StyleGrid)) +-----------+--------------------+------------------------------------------+ | id | name | sentence | +===========+====================+==========================================+ | 100 | Donec Vitae | Quis autem vel eum iure reprehenderit | | | | qui in ea voluptate velit esse. | +-----------+--------------------+------------------------------------------+ | 2,000 | Quaerat Voluptatem | At vero eos et accusamus et iusto odio. | +-----------+--------------------+------------------------------------------+ | 3,000,000 | Aliquam lorem | Curabitur ullamcorper ultricies nisi. | | | | Nam eget dui. Etiam rhoncus. Maecenas | | | | tempus, tellus eget condimentum | | | | rhoncus, sem quam semper libero. | +-----------+--------------------+------------------------------------------+ 1. Unicode. tbl := New().HumanizeNumbers().MaxWidth(20) //.ClipCell("...") tbl.Header([]string{ "id", "name", "sentence", }) tbl.AddRow([]interface{}{100, "Wei Shen", "How are you?"}) tbl.AddRow([]interface{}{1000, "沈 伟", "I'm fine, thank you. And you?"}) tbl.AddRow([]interface{}{100000, "沈伟", "谢谢,我很好,你呢?"}) fmt.Printf("%s\n", tbl.Render(StyleGrid)) style: grid +---------+----------+----------------------+ | id | name | sentence | +=========+==========+======================+ | 100 | Wei Shen | How are you? | +---------+----------+----------------------+ | 1,000 | 沈 伟 | I'm fine, thank | | | | you. And you? | +---------+----------+----------------------+ | 100,000 | 沈伟 | 谢谢,我很好 | | | | ,你呢? | +---------+----------+----------------------+ ![](screenshot1.png) // clipping text instead of wrapping fmt.Printf("%s\n", tbl.ClipCell("...").Render(StyleGrid)) +---------+----------+----------------------+ | id | name | sentence | +=========+==========+======================+ | 100 | Wei Shen | How are you? | +---------+----------+----------------------+ | 1,000 | 沈 伟 | I'm fine, thank y... | +---------+----------+----------------------+ | 100,000 | 沈伟 | 谢谢,我很好,你呢? | +---------+----------+----------------------+ ![](screenshot2.png) 1. Custom columns format. tbl := New() tbl.HeaderWithFormat([]Column{ {Header: "number", MinWidth: 5, MaxWidth: 10, HumanizeNumbers: true, Align: AlignRight}, {Header: "name", MinWidth: 10, MaxWidth: 16, Align: AlignCenter}, {Header: "sentence", MaxWidth: 40, Align: AlignLeft}, }) tbl.AddRow([]interface{}{100, "Donec Vitae", "Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse."}) tbl.AddRow([]interface{}{2000, "Quaerat Voluptatem", "At vero eos et accusamus et iusto odio."}) tbl.AddRow([]interface{}{3000000, "Aliquam lorem", "Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero."}) fmt.Printf("%s\n", tbl.Render(StyleGrid)) +------------+-----------------+------------------------------------------+ | number | name | sentence | +============+=================+==========================================+ | 100 | Donec Vitae | Quis autem vel eum iure reprehenderit | | | | qui in ea voluptate velit esse. | +------------+-----------------+------------------------------------------+ | 2,000 | Quaerat | At vero eos et accusamus et iusto odio. | | | Voluptatem | | +------------+-----------------+------------------------------------------+ | 3,000,000 | Aliquam lorem | Curabitur ullamcorper ultricies nisi. | | | | Nam eget dui. Etiam rhoncus. Maecenas | | | | tempus, tellus eget condimentum | | | | rhoncus, sem quam semper libero. | +------------+-----------------+------------------------------------------+ 1. Streaming the output, i.e., a newly added row is formatted and written to the configured writer immediately. tbl := New().MinWidth(10) // write to stdout, and determine the max width according to the first row tbl.Writer(os.Stdout, 1) tbl.Style(StyleGrid) tbl.Header([]string{ "number", "name", "sentence", }) // when a new row is added, it writes to stdout immediately. tbl.AddRow([]interface{}{100, "Donec Vitae", "Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse."}) tbl.AddRow([]interface{}{2000, "Quaerat Voluptatem", "At vero eos et accusamus et iusto odio."}) tbl.AddRow([]interface{}{3000000, "Aliquam lorem", "Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero."}) // flush the remaining data tbl.Flush() +------------+-------------+-----------------------------------------------------------------------+ | number | name | sentence | +============+=============+=======================================================================+ | 100 | Donec Vitae | Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse. | +------------+-------------+-----------------------------------------------------------------------+ | 2000 | Quaerat | At vero eos et accusamus et iusto odio. | | | Voluptatem | | +------------+-------------+-----------------------------------------------------------------------+ | 3000000 | Aliquam | Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. | | | lorem | Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper | | | | libero. | +------------+-------------+-----------------------------------------------------------------------+ 1. Custom delimiter for wrapping text. In this example, `complete lineage` contains a list of words joined with `;`. tbl := New() tbl.SetHeader([]string{ "taxid", "name", "complete lineage", }) tbl.AddRow([]interface{}{ 9606, "Homo sapiens", "cellular organisms;Eukaryota;Opisthokonta;Metazoa;Eumetazoa;Bilateria;Deuterostomia;Chordata;Craniata;Vertebrata;Gnathostomata;Teleostomi;Euteleostomi;Sarcopterygii;Dipnotetrapodomorpha;Tetrapoda;Amniota;Mammalia;Theria;Eutheria;Boreoeutheria;Euarchontoglires;Primates;Haplorrhini;Simiiformes;Catarrhini;Hominoidea;Hominidae;Homininae;Homo;Homo sapiens", }) tbl.AddRow([]interface{}{ 562, "Escherichia coli", "cellular organisms;Bacteria;Pseudomonadota;Gammaproteobacteria;Enterobacterales;Enterobacteriaceae;Escherichia;Escherichia coli", }) fmt.Printf("%s\n", tbl.WrapDelimiter(';').AlignLeft().MaxWidth(50).Render(StyleGrid)) +-------+------------------+----------------------------------------------------+ | taxid | name | complete lineage | +=======+==================+====================================================+ | 9606 | Homo sapiens | cellular organisms;Eukaryota;Opisthokonta;Metazoa; | | | | Eumetazoa;Bilateria;Deuterostomia;Chordata; | | | | Craniata;Vertebrata;Gnathostomata;Teleostomi; | | | | Euteleostomi;Sarcopterygii;Dipnotetrapodomorpha; | | | | Tetrapoda;Amniota;Mammalia;Theria;Eutheria; | | | | Boreoeutheria;Euarchontoglires;Primates; | | | | Haplorrhini;Simiiformes;Catarrhini;Hominoidea; | | | | Hominidae;Homininae;Homo;Homo sapiens | +-------+------------------+----------------------------------------------------+ | 562 | Escherichia coli | cellular organisms;Bacteria;Pseudomonadota; | | | | Gammaproteobacteria;Enterobacterales; | | | | Enterobacteriaceae;Escherichia;Escherichia coli | +-------+------------------+----------------------------------------------------+ ## Styles **Note that the output is well-formatted in the terminal. However, rows containing Unicode are not displayed appropriately in text editors and browsers.** style: plain id name sentence 100 Donec Vitae Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse. 2,000 Quaerat Voluptatem At vero eos et accusamus et iusto odio. 3,000,000 Aliquam lorem Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero. style: simple --------------------------------------------------------------------------- id name sentence --------------------------------------------------------------------------- 100 Donec Vitae Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse. 2,000 Quaerat Voluptatem At vero eos et accusamus et iusto odio. 3,000,000 Aliquam lorem Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero. --------------------------------------------------------------------------- style: 3line ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ id name sentence --------------------------------------------------------------------------- 100 Donec Vitae Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse. 2,000 Quaerat Voluptatem At vero eos et accusamus et iusto odio. 3,000,000 Aliquam lorem Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ style: grid +-----------+--------------------+------------------------------------------+ | id | name | sentence | +===========+====================+==========================================+ | 100 | Donec Vitae | Quis autem vel eum iure reprehenderit | | | | qui in ea voluptate velit esse. | +-----------+--------------------+------------------------------------------+ | 2,000 | Quaerat Voluptatem | At vero eos et accusamus et iusto odio. | +-----------+--------------------+------------------------------------------+ | 3,000,000 | Aliquam lorem | Curabitur ullamcorper ultricies nisi. | | | | Nam eget dui. Etiam rhoncus. Maecenas | | | | tempus, tellus eget condimentum | | | | rhoncus, sem quam semper libero. | +-----------+--------------------+------------------------------------------+ style: light ┌-----------┬--------------------┬------------------------------------------┐ | id | name | sentence | ├===========┼====================┼==========================================┤ | 100 | Donec Vitae | Quis autem vel eum iure reprehenderit | | | | qui in ea voluptate velit esse. | ├-----------┼--------------------┼------------------------------------------┤ | 2,000 | Quaerat Voluptatem | At vero eos et accusamus et iusto odio. | ├-----------┼--------------------┼------------------------------------------┤ | 3,000,000 | Aliquam lorem | Curabitur ullamcorper ultricies nisi. | | | | Nam eget dui. Etiam rhoncus. Maecenas | | | | tempus, tellus eget condimentum | | | | rhoncus, sem quam semper libero. | └-----------┴--------------------┴------------------------------------------┘ style: bold ┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ id ┃ name ┃ sentence ┃ ┣━━━━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ ┃ 100 ┃ Donec Vitae ┃ Quis autem vel eum iure reprehenderit ┃ ┃ ┃ ┃ qui in ea voluptate velit esse. ┃ ┣━━━━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ ┃ 2,000 ┃ Quaerat Voluptatem ┃ At vero eos et accusamus et iusto odio. ┃ ┣━━━━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ ┃ 3,000,000 ┃ Aliquam lorem ┃ Curabitur ullamcorper ultricies nisi. ┃ ┃ ┃ ┃ Nam eget dui. Etiam rhoncus. Maecenas ┃ ┃ ┃ ┃ tempus, tellus eget condimentum ┃ ┃ ┃ ┃ rhoncus, sem quam semper libero. ┃ ┗━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ style: double ╔═══════════╦════════════════════╦══════════════════════════════════════════╗ ║ id ║ name ║ sentence ║ ╠═══════════╬════════════════════╬══════════════════════════════════════════╣ ║ 100 ║ Donec Vitae ║ Quis autem vel eum iure reprehenderit ║ ║ ║ ║ qui in ea voluptate velit esse. ║ ╠═══════════╬════════════════════╬══════════════════════════════════════════╣ ║ 2,000 ║ Quaerat Voluptatem ║ At vero eos et accusamus et iusto odio. ║ ╠═══════════╬════════════════════╬══════════════════════════════════════════╣ ║ 3,000,000 ║ Aliquam lorem ║ Curabitur ullamcorper ultricies nisi. ║ ║ ║ ║ Nam eget dui. Etiam rhoncus. Maecenas ║ ║ ║ ║ tempus, tellus eget condimentum ║ ║ ║ ║ rhoncus, sem quam semper libero. ║ ╚═══════════╩════════════════════╩══════════════════════════════════════════╝ ## Support Please [open an issue](https://github.com/shenwei356/stable/issues) to report bugs, propose new functions or ask for help. ## License Copyright (c) 2023, Wei Shen (shenwei356@gmail.com) [MIT License](https://github.com/shenwei356/stable/blob/master/LICENSE) ## Alternate packages - [go-prettytable](https://github.com/tatsushid/go-prettytable), it does not support wrapping cells and it's not flexible to add rows that the number of columns is dynamic. - [gotabulate](https://github.com/bndr/gotabulate), it supports wrapping cells, but it has to read all data in memory before outputing the result. We followed the configuration of table styles from this package. - [go-pretty](https://github.com/jedib0t/go-pretty), it supports wrapping cells, but it has to read all data in memory before outputing the result. We used some table styles with minor differences in this package. - [table](https://github.com/clinaresl/table), it renders colorful texts, and supports Multicolumns and nested tables. stable-0.1.7/go.mod000066400000000000000000000002721454160570700141110ustar00rootroot00000000000000module github.com/shenwei356/stable go 1.17 require ( github.com/dustin/go-humanize v1.0.1 github.com/mattn/go-runewidth v0.0.14 ) require github.com/rivo/uniseg v0.2.0 // indirect stable-0.1.7/go.sum000066400000000000000000000010071454160570700141330ustar00rootroot00000000000000github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= stable-0.1.7/screenshot1.png000066400000000000000000000354541454160570700157610ustar00rootroot00000000000000PNG  IHDRk pHYs+ IDATx^wTWʂ(PآE KlIk1v%QƆ=[4֨{{G (ݝPqYUrs8Gݙ};w%A,M;AAzDAkv_Ӻ2ݐ`%+dio=ZNlł1D2ȯݾkA!%DFs2&RsmQZF}D;|u\"BNfdwp\n[ciȶ'RΒVO^Qw& -ͅ%ܸȯ(%S2ޤ} ۊh e._!'K}1kAl@OZAjDZ!Z!Z!Z!Z!Z!Z!SMiu }ʠ CĐVTSPPecҙ?_Խe %z19h+#ĥB6MQIwOtRNH7) 6N\Afn\tSZXwC7_ 5HKOkrF;עR8D"1{D|&Q=)gim~ӝx4<3,&@=G0k ({zF3/d{: >3;bVZ72x>7&Q.'P]q̈́,Y?;Zweq\'~☜#W63O[vN>z0jo2+~+po.9S- A0IFr{wjϟca+WT÷ѳa>wmJw܉M&d?jH>ΈV-hڼ= uz Jv=,15]h{!1ʤ'|!;b>"n4kNL%?98槤\4?;%VV/.!ZØE(RywFMSB5<{OIT 4Қ62de(6\jGZCX&8]LѠW2PIq<ϕɽj~^4ƍR8j$⟜aIv/dg: >2k΋#Fr9ؘ70FLL:93lT:? Jv4T݈Z-Z$ FȑPʐ.IQM 3A raSõ#o=ݧojOI9!Ow a[(B7}sL( w"ؐF1E^"Ncs a `c)2~͕!g2H_<"w#sE/3""7-ւ BV#~ " b- b- b- b- b- b- )֦4:Ɔ>eP#pbVw+o~<-]WaUWی]?W2U;;s<j,u8‡%C)r s*3cg3s2}s~e79h+#޹f[ӂ8@ʆ=V~t|۠LO'wSᣨ2zN Cl[> 'k0GYS[^Cb"ͨ>`&+2Åv^%x vvt>mLW ® y?eeϝۓEjs8 Թh) <4i72WT=DdhTF vݠ*[fMaH,JͶMz9ݻ+\Q;.p޻-yy(Z4dVNtg9w(uAm\y*TϞuO8Ά򸫰kΘ  ?#iPwt]ϊd [vḧMfoYOe8EF麂K:R`RAXu?|9}hte;Bz7ٻ0G7fޛ_eoؙE=ߗM*i~~3 \ߍkOgա<|˫W%1Q!2tOW[,=C}' 5qV7=9;Wyl*dpT0-BqX?C+O \$#[Pݽ; ^1ͰI_gfѾW D櫟оnїt%U7-b:O9~`BZҝ8?uq=/YZIi\ 6 s~&dTVJg]MTxye3^m*3֗&ӣ%ߵyF-F7m;rڴ(h+yYAfehԥ`~ nrl`8-eXp9>?q4J(N6NnPHOVwQ*/ 4]xN}zI]g{•;Zգ$ Y׵7klF"=%P"y6n8IU@뗞0jÞiR2{ \Sۂr2**&Z $H3Ȱ˚h4q> +T*;6 ;B!!Zz ;8m9؜|GhdқHjjP*_a/T!d.$W`jjcjf"MA~Jn '}aKAitn?rZ雜&*W%߽`j@z'J9 3%]4FwF^H)!bgٓF6W*\:ay* j˪9觢eN8XQwjai+\)AGOG8ˉ6yjDԆA=%0)(X/7%*#1o^-F2 !/H7lJt+7)b|.C0N>w/1o s @<g gwXdƇɿ64OC}=K }| < IA>gDy0kI{(ڔ}}DNA 83(,$Ҵ6?1N ~u=F_%cOUCDx^ ,'-~XVNϮi,C@ %Ddp yD2GP߻Yܳe0QڷaA! ـ(ւ ـ(ւ ـ(ւ ـ(ւ ـbkZWsݿ`%+dio=ZNlł1D2ȯݾkA!I(FFs2&RsmQZF}D;|SݲExC/s 9QwpWs꺹Kn!۞H:Knn[=zeGޡ4pWVSK7vD\/s""۷rsMǀ`O;nOD2ȯݾŘ B6g- d5g- b- b- b- b- beXpFu>̡Y$3XyNzwΘ )D*eP][˶MU^kTh2p+],T YӔ^Ч Sr̟:}؅51DFi5+(El)/[wRj) 6pfn$K[`v(CӮٍ6<1ACUi5+xl C`ɶPjmB1yij4%+yqڲu"C"1GN1(]9ơ.Wc.xxb.O1ғKtbLL,U'ZIH||%3w%sm衿}lVo˾(}UgvL]r^{wπo*uAݻ;ܖ+Y1SaC\5o_%r=yCl-[W|E.,ܹ]Zckۖ;NVu]sƬO@`'I7맠tq,ߺ~>SP*y|)ɬwvLhLQcGWe,=C}' 5qV7=gmjco,]zqL ccDjϽ}V}Hל jQC$;'J8:T4SqvS%Gr5׷NgT2+~+po.9S- 3M;鸛]+67={YoP%tC,O],?-/`plBu,x՞?4&&d?jH>ΈV-hڼ=_N1TJ}{Fv^lwbX\LӖz]ӢZ|6h&Mפ<𱪬)Ǭ55`;r*≉P(037C YAEz6/_\)UJS,PΜ8n%P~@5* E-E]^Ҙ%/薫@><֝ hBY$OÞViMA<}ufp}Nr#F9Ąh 6ˆQl깔5#&8e_΢2;r~,.zڗƄh++ŷcGӨJQ% r\*G^y^$ϟ{]W3\N3Nl+fU*qHgrw~.Qp,R]-'ɴ/w3.vd Ō)N'L:a֭T\MEVAh9rOy3֭h)ybؚiZh2T XY . (/#ޞ4U'+3g- [@d\ɎE(iz<| KV.p}ase YXK/p;0eU\[џ_)}idz9]ExnsUIZs@|{% iÒg ýj^7ei (T6M  C+)Haxpu]{&I O,/XWٱ9 0ޱ ,.0,1~rGlχC LM03QrL05U&&cP:Oխ5D/l 9#mijsW<8ur#k1ӓ%.=Kԛg$}]3w kTq $ߛ?)33S2)L033DS^q/$R6 OsX;*Z+AfJ^*4iZ)?bZ0"bX9@~\`|TqFR'>cϔ(=i8]+m&.c9|_`cޔ~:2؊|ؕsfy}&xL{xgurvM3K+WFXey)(Ԓoӹ,=d-U+`gΓ{9V˅sTϜbݖs.,;<$4v bQϘ@g(f8IQv/Ew_gxos1N?̦AUS+8sƺ|}wo9Ư!=e>J'f|?kXs$=OTxی_p}d5giԽE4s>a/=}KEQ, o7Ų#JS\Pzd>y'o˰8%GWx+G]w*Ti5/v;pJ47c}M wP%t.ҏy,#/rι+IܟߕK>&B!lKOZAƞ ʦsJSG:Ʀwׯ4ȵZX/_<]4[A|4q)TD:.S#B.۷b- d5zQAX dX dzu7!D~z1IDATPN{5 io=ZYޖT[9c[F/‰A\>{-f-\sQ! ҘZaW^sRaQЂu#\:@z~BDrE>l)D!/څUlREٛ9w{Y;=NAn$p^ ?.Mj7kҸ{A9IIƭ_OƗϏYu2_UkIL?DrݝwmZL.rJhӕՀϤf4s U?tchZW)?NJF(N}e}kI*uؽ|#ף|Y6ۆ`lK's\)njD2)<5I7緳/ /F $ZÚc_cA0<Fa'KHN'v7!Wڝ]cӶ[!u)(8Aw$ c(!>*2{7{✱>&QJ"?YFZ[0 ՈaAl@kAl@kAl@kAl@kAl@OV`5+빮_0_r۷b-L~tsNb"KWnz $_#pǹl^@ʋ6x-h#>^n"!D~m(;8J嫹Hu%WҐmOy%77wխTdPM+ZR)⥛ ;J".q_qQs &c@'V'MID2ȯݾŘ B6g- d5g- b- b- b- b- beXpFu>̡Y4f,)twޝ?ǒSqz;m46Aw9v]Օo_:3)v52`4FloJoE?y.L[;RR>VU ET%1ooNahEqޗsSRB:tۜ-b⯋8@*5VQ 9i[I%!yKI>2o$/7}&$oQ>C6PP^̶ 'W7Y?L7kTX)ckBa8n;R.̕,2^]21v/Z.HýM?j7 UjoX}lR.CB*Ed%uBziR4:Ɔ>e0<XKCS6o.]R5Z=SjumIŘ`,\ϯu-Q̕I.bR~w;ʟsL_>v*iԝkb ?KTBets6kB3#wՊ|r&Vp(_.L!,=mK<ꮔ5-~1KQ>¡)޲N VN]Q3\(K~7}kHLu]dtţ EPVl^:݆Gh 14z+OLnB -ώa+Ka3Kdu5}tfT 3YGn𹋜]Wq+$|7͓*PC{5/3d(QecJqK2y1\6 ^fC U 7&fE%z#r[O1/KVuE0F?;i= ?ohK<8PW8ۯxLT5f3 F7JVYfSRKRmɍbh38VmYy!~#'И@|\ϮD9rn=쒏J*%Yd>?3ǬJO/Y%Y23e33e%f|;9s0g򏴮q+jkj\r[_BKcXB?Q J]Sh_Vef|/&NŤO0'9'2'eaHFhyr"<;'M$"?IV`W&wdSHwlr ecUEEV#oO9Pn߉BMw\`(mOi!OoU eaj o8U`⪖^ƶ:Ο!dfۖkٳ琿( 5 Oe8]Jov$|'?6SH𽷭üb)l*4) c eJt8 ݾ!>7nGƾk+pJ+ wosWc8n48v5p+yIZs@|{k_9yRw|j υ@?h٤ UzfV󮡬waN_t|߾%-ehfJckQgڇwlIZW/tLM~3QsCaR'&YȀJubJLjzaf䘚ajHzu>R[/j*É*w_sGZ#⯙8cZVmFєkoh?sW<8ur#k1ӓ%.=Kԛg$}]~SXcexn$w =pSHٵ|) ?z~%%{лiYuCq߹>-\Bڰ+l?zp5)M&fݻ.}Y^{C:_qfPFmV&GU:6qs;GБsnppf|HFo|3Oȇ]9GjvhOcOo<)ۭ7lCػv/\S[`)zD 8t.K7DFRpv<0,bC5%>yGY~؛Nwn;v!$eoVY6cVpp-L#[Q%y 8P;IE@.aQBXwņ/1z2e)^^ "䗌܅R^(d N<5t'{ vaïdqLrV6#<^g/14z)_Std\/zʎ_f/KzIJJ^ON[soO}9L`N$A:wvgI)~= %CpLSFH`R S s~)-#<)<уz†YS;&'C]+ѫ[*kF7r*5.\M7djL9s3 uCqý(d<סJJ61TAwD (a)yHHZ? UiZݎ)Ac}M wP%t.ҏy,#/rι,w9x(=g~mmsNfݭxI1r9Ur; Kژ DJy I|CcUӜ@ltjb_F2&ܺt={vy!F&}G)~aC]3H K#Ps3F`Nq+՛ E3jIl:厔t.yM|_i-ˑkh$_x"#?+14hRΪTDC%#Or[ AݾkA!A7f-Oʚ L}c|=Cpxh (TIKz 7x#" |ӺibG0v+.BqVs;qš]saIׂ1D23ԥfqkֲG{S#ne^Sga9̝'A[P]*Qeba43FмkZKunH#E?(?B4Xq_\# 2uA'ݡ1Agv6*FvbࢢT'&M}&WiS C0̊S>~~>*HSsZG _q QsQ_51s/'u&ع9a6 )to %|Kf_uz̪SݾU߆)Oxq{y(*u" %.N<[h9Ulxv4גt/˩ּe Y2$*< 9`NQ{1\ )B}HqR11ԤA̓EB97 ߀/!Upã]܅ѧV^LeK:%9QznC^.`skkg-/yߙ@e:G‚gv>!Z{mgֺsoݏ~J}Ωb-J2T+uRT<%`nS❟Z^笯/'vmq89aĉ0 S Wo7*~x܆m{js CojC> UűRALI $fz|< o}cɵ5>\M$Q fqqQR.ГwS߯\IҠIgQ\"7ТVH 9z cbS+/w)\ Ѓ:Z'nв8yȑIbbǺ3&jr)=Nׂ1D2G7_TO2T[OAqQ!Z!Z!Z!Z!Sع~MV>_r۷b-L~tsNb"KWnz $_#pǹl^@ʋ6x-h#>^ oe._!'3};8JRj.R]7wխ4d)p^g]ru'U(;TjuxŽen\Wsrpv\nnBIIx.%+dio1f- Y YY dX dt/IENDB`stable-0.1.7/screenshot2.png000066400000000000000000000334161454160570700157560ustar00rootroot00000000000000PNG  IHDRkaz pHYs+ IDATx^wTW.eEl((AQނ%$VĞX1XHԨƮآFb{G (Rww?@eqQ>pλ3;ΝwUX.OEAȹ$ B# B. B.`X+sU,Q臄 ^"B^~6P%ʶG74/2B/{ yY@ArEGQ6 R\CLtQlo$y"JW2ؾ, ]ze7&?:e7wF'=y2M|e2B0JgT+Wjgmls?U%ſlTݚ{7ROGZ9ʇmhՒf-Z#Pgvt_YÂZR˕JE -_ͲXT܃&Cwqit\G#iwbm8gΡ4&Sٸ8ȡۤ8˗@ #\ǣGф=#Ih+X+Pigb8s kspbj 2V`IE-?377eh#5# NMghBn o <|10DDx'P(UgT3[ "ZFW;QCNEj  JT6&8M@aZwK ӦF1O/4q*J6Dx1"˗bS4{w ޸7<l(2Zכ$'XSZ(-`B"VHט2D*R —ؾ)D0B\ @xsڛ y*g`,n ?>0Td@E"0ţt1$)F/zƚۓq;4 {:Х|Օ"*N_ЭE_ EnnKaPRN_ihj H.kX;*Y@aJ4mVB?SbIrV*dHjx<ˏAT T}kCع,z{B(XعвnId/5I9փ@Ё[3gNfI|Ɨ){vao= ulF*=g@x2kqkhڔS2R7e>ͩS~g&mJRG{c H\wGqp^е:ֳvr*8{V~mI/t~)Ɣғ.KCd oA/{ yY;zւ BVSZIy쵉B&:$ _.d_ڷaA! (ւ (ւ (ւ (ւ b ZW%+eoZl~tsJb!#Dȯ߾ kA!Iy(f:a*uu1$@ŶYb7e5"&.q_޾I`-`\F!|>:tF|'E7.+y9n}?MLHLӑð0\AN;Af6,㭙|R/""w2 b ;/_(y3NrvVMBUW3* uOn܇9붱g!oZ)_,ׇ =$va:)\N~00 | ъ|<>/pi\f$M#%o:^HpLn5B *֪kܹe(4۴޼C%o] g0"PѴ8%m-'Rʕ3Y{~Usjά?@tOs^>9*bPUTmߘiЋR'us j[nHw:ʢN3z^´0fӤ6.m~"kUP޺Rzyn!*m#&&i{WJ3W՗&ӣ_EƴPoviY:e$JlmҾyeYY]b1fqdi-t·b(Jš1G>xަ#<ϋQReJYǿT)iGɢwEQ 5a%~AN>Dk~]$cYv]&,cc^4RZn\;qh^`ﱻ92~ZX@Ι3s(k#ҁ+w8~'stj^!`QfuٵlWbth"|Y&FL\eVdM?kGTGPPϛSٸ8ȡۤ8˗@-GqUdbn&B!P'8(eY<=u/t~I>ꎜj2=7z2赓zv̒ jTQyMDx'mQ"mv?"99ibZaZwK g>I0VNg% mat+=W*<~9,I THT%&r~VB<{ a}Y~=a!)5ǜx21,$s!8;l[ѻXñcI)󼁙 H-} Jz`{#-Fkt<>#ÿ~xi{#cl!gdʒԬaMrn&xoZ~rQp(IoMu[ܢM)9C.FݤB&4 G@<ӆr#\u ϝ%J5 tp4^QJ S Uiz2CwqۜU{/W\Xu>K0R-/z?5ꄣpVGHHRtrұ;@/}& O]m9V %E囆@dM~ۺ]QĄ}[}_}Ou+ ⠖PiEE|ls~3u )͘H0H pp^uan|}(ly`cEb4Su>,&1Bf4)K֔;Ô>?c%J d]Vp) 9P*7X>KpZq2MCҾt0?#*<`fwZO=y2M|e2m#tY_9?nEtwe^sjά&Ԫm!к+;1SrT|W8ZR~w?koccZZ9ʇmhՒf-ZcхTCE*6poL[4z:WH)2O/nbRqn>{0TcoؾT$ Y8kd`J{k^HS'Qek/nVdL'g·b(Jš1G>EAɅb=OR)TU`kz4?O`NO<6_T%Җ:cֲ{_>:d@F6GфEQmEkuvre.s}F;ƄK4F1l칄ӏ}aaoޢ;װNZ.]z:+hqch\sY0S@q')}/ʩT>clNݳ#4{ky}Ê\,{/rI3btqaF>$BCnq$U|r㹆TU`_9-aWۙ ]ʸK]ȌsY821_d)piiG:̺wA!jZ@BFBz_ؗc:RHJ`cbݴ~&ugP@|&Pވn[82;8/w''{9PMnDáöHJ/npKx sZtƱRiZ;, 4MUZai{n~$)ffP`ffIvU!?vh..^]6+װvTR”vUiڬ:Robz_ɑD@%۷X9@yq/BI:}-R2HVS&0}ot-G1mMAe oY773όw]3qǡ@}y3fD ad]"qcBg_9?ˡ<ׁ{lӨwٛni4}z0g݋Y ߛ&׆'ߗgEGKz&ͥ[';t}9r߼c~Wgu΋%yIIDAT~~ uZ)HE a,lV11:'uH>NJفoW3+5Kh&Bպlr=73гVbċڑ[wJM<\2]ȰLOt:tXP`Y]4U[AsS/ZBMV]D~K,ͷ=&L5I}(ւ BN7A! B. B. B. B.`X+sU,;>"KWoDbBFe/_!/K}(ւ BNP4u51TbIЁ.m#׾'+)Dȯe}X.;8:JhlS1-j .Sy=>ĝ)vZկ7 2uGMūKm7qEɾ SF.K)^&_1 *8r7mӷ FwJBK2EUa ,{v8wOԔtSؕD6`ץk4O&cOT Xaԟ l;<=Jsl·da_F{o$)̗}"-W)LU8O5`fSJ)ΟIеe*WDN@)rp<(ƀh[MooE/PU曉ip 1,.Hi5xyJڨyt 嗬:2f!{¯z2IՎq3R*xT/@GxZXH2+9﷍/}2ФLi}}b`31`~-]DRYcJP)6|p\z]Gsiv2̹;70O<>bU1/ޒ;V3H>2;&I%3V&zmLax%W1y->gN?%{:Mo\9/Y{K7+*;*#{#$ f|;SFs׎rj(F. )vZѬ}:z}l̞͜ٿ1Skh _lse=+"%|3x,3gE߾nUf;(fB;irԧx527g'X4ڊ)CjR$^ Am?3oG%! N9Wp< V1A5hΧ7yvb'$_"@#aiJ}w71GabEYbiΟ .Rrmn>3<gg1) RfjKXX|_٢S,ZzRԞ/q\04<ߡ'qbR.fC6 =y%Kqe5|qȯ_@/?韐#|O㜹n%K=?򤐡te=5J~nߛ&MY>y% d"qSZ@B| .Ll)tiw\cI*_z TR='+qur3\¯-"iC盯x3n3#V W_B3KccٕuM̮;ZnӇ6_;ǞX]mnOʆ 잀ߴxH).bRyEZ'v >R~L)WbW(&vxch6tlhOtF!G#x1UI=AV$QG9x - r;%t|Y7u,~SB>&)Ҵc] {*Ԑ q~BctQrU87n&PV}\ε(@1l<{i^ڗsVNGI4)R-<[KYklN)艳sek}9qSqIsn!j8|5r%.Sz~èRRqz܆g{)gsۧOW~'ԌYA[t{_}T?.e/霪P]Qs4MTD^=͹80(WgKz*BkVREdި!Q ia7q4Z<늋3 $ߟ[Zc.|j'kӨTJYe}'&MɃ-ku*ϟ>%#0+@ ;{V c2( FWq3|ucNA$--O$u>$@Tbfn&.y>)Ǎ16fCɅb=ORLUU`v /39 LA7/[A l)qwOluD;)yҢMu;VdOM'O?uZ?K(([-ϣG-E[a5N_dTl[/p19U!J$x:Lf@ yZ; BR5dM*KH5QVADu/({2~' ; JYc#F9ԁG񜱋N{r>e"8q!ndvk SL𢨛P{ƏBMhoj`:+n_>!-XOKf3K"4<@RQG+cl>ϫѿr$Z®3%q%.dƹ, R^Ђum^ KK;a \*5 tBf(+v.ׂsT ӟAOf7QuL0)2&ϽO3?:Yzf=^0G1u^dÃX GWbhj| 7 Jn $G+cRgAmi7{S[i%2MGqjǸUQI.k[ m/@ _;٤*"##aWv=u6n-2]g{,yMnDáöHJ/n$_cl Ӳ3$JEun:h/OAG90-q1` {04J՞ 'S&e,+]E,=ߴKi [2Xz2wtvԪeƩ^:o;ۮiU};9[ogG͘]LfhG^Cga`$?':SIryxL]sb(66] 2 ?9ŢA_-c8-If&J@ SSehFn$'_jC2ĉaոئ^{cg6>ƙXGyݞ"sib^>fn;Oi:I|'a߷i;-DWQ~z7+~Ȩx3_F[ MDqmSSó%_VRiLyEYsPzƩC:L lަffHҭ!wnH~צxu?Q?0%u t<:eGAMۡ܀9Z_2zd(q $`ާX7c_ M ÙzՅ] >^JX< v坨ա=u-S?&Q[oۄg^Z1bd[jT="%?i?GF WؙGWZQ@ҵe<3CM25 /2rboÑO|=m.gw1FQ:* vN=gsxDR(Wʈ'X{њO2ᛃ{2{^&g{C/4$۟¢8v%g\tp~"֥^Մ]ᴯ/vuQ;;c̱gJCٶ'?Nt+,N:nU~ҟJ؅?R3aXӜD?T@sܛ?Cj1x7]LFF/Ye)WKT1'ڝQOL8svP}>Gui &%V%`A&#<‹b=yj l՚! *=/NkUE˥acV IC9ߊGg/(M](onF.ck0b7EO6Rgx>rPE=֋j;0x"7[BIpra5,U E~!_jMTO2s >{< oɡ յj.%ZN)l(_HGW_scEmxYZ uZ)CWRB>?)bUbp~Ns],ϱbv`F2wvb Mb{\oǿq-՗#ش]^3iU8_.InfW`P fLE!ͬ GD>V}ޥ>=-bߑNCh&r{v6m9ȥ]nsaT 8Tnͭ53ud(Ž2c+pa y2:4 Zt<7aLlfI5`B:. ?o+Ө9|*tRO %AJc=:{^j~Q}/jGnac`TgS'O:742,$$,@t۶jfԳTРyHdC \;y !}9lU*y@߾ kA!AHkA\@kA\@kA\@kA\@VbXxH'^"B^~6P%ʶG74/2B/{ yY@ArbQ6 R\CLtQlo2N!D~,~rIPUQ_vh"PQ_vpkTq˃v>5Mڂeo\Wrxv // // 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. package stable // The data structures are similar to these in https://github.com/bndr/gotabulate. type TableStyle struct { Name string LineTop LineStyle LineBelowHeader LineStyle LineBetweenRows LineStyle LineBottom LineStyle HeaderRow RowStyle DataRow RowStyle Padding string } type LineStyle struct { Begin string Hline string Sep string End string } func (s LineStyle) Visible() bool { if s.Begin != "" || s.Hline != "" || s.Sep != "" || s.End != "" { return true } return false } type RowStyle struct { Begin string Sep string End string } var StylePlain = &TableStyle{ Name: "plain", HeaderRow: RowStyle{"", " ", ""}, DataRow: RowStyle{"", " ", ""}, Padding: "", } var StyleSimple = &TableStyle{ Name: "simple", LineTop: LineStyle{"", "-", "-", ""}, LineBelowHeader: LineStyle{"", "-", "-", ""}, LineBottom: LineStyle{"", "-", "-", ""}, HeaderRow: RowStyle{"", " ", ""}, DataRow: RowStyle{"", " ", ""}, Padding: " ", } var StyleThreeLine = &TableStyle{ Name: "3line", LineTop: LineStyle{"", "━", "━", ""}, LineBelowHeader: LineStyle{"", "-", "-", ""}, LineBottom: LineStyle{"", "━", "━", ""}, HeaderRow: RowStyle{"", " ", ""}, DataRow: RowStyle{"", " ", ""}, Padding: " ", } var StyleGrid = &TableStyle{ Name: "grid", LineTop: LineStyle{"+", "-", "+", "+"}, LineBelowHeader: LineStyle{"+", "=", "+", "+"}, LineBetweenRows: LineStyle{"+", "-", "+", "+"}, LineBottom: LineStyle{"+", "-", "+", "+"}, HeaderRow: RowStyle{"|", "|", "|"}, DataRow: RowStyle{"|", "|", "|"}, Padding: " ", } var StyleLight = &TableStyle{ Name: "light", LineTop: LineStyle{"┌", "-", "┬", "┐"}, LineBelowHeader: LineStyle{"├", "=", "┼", "┤"}, LineBetweenRows: LineStyle{"├", "-", "┼", "┤"}, LineBottom: LineStyle{"└", "-", "┴", "┘"}, HeaderRow: RowStyle{"|", "|", "|"}, DataRow: RowStyle{"|", "|", "|"}, Padding: " ", } var StyleBold = &TableStyle{ Name: "bold", LineTop: LineStyle{"┏", "━", "┳", "┓"}, LineBelowHeader: LineStyle{"┣", "━", "╋", "┫"}, LineBetweenRows: LineStyle{"┣", "━", "╋", "┫"}, LineBottom: LineStyle{"┗", "━", "┻", "┛"}, HeaderRow: RowStyle{"┃", "┃", "┃"}, DataRow: RowStyle{"┃", "┃", "┃"}, Padding: " ", } var StyleDouble = &TableStyle{ Name: "double", LineTop: LineStyle{"╔", "═", "╦", "╗"}, LineBelowHeader: LineStyle{"╠", "═", "╬", "╣"}, LineBetweenRows: LineStyle{"╠", "═", "╬", "╣"}, LineBottom: LineStyle{"╚", "═", "╩", "╝"}, HeaderRow: RowStyle{"║", "║", "║"}, DataRow: RowStyle{"║", "║", "║"}, Padding: " ", } stable-0.1.7/table.go000066400000000000000000000662011454160570700144250ustar00rootroot00000000000000// Copyright © 2023 Wei Shen // // 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. package stable import ( "bytes" "fmt" "io" "math" "strings" "sync" "unicode/utf8" "github.com/mattn/go-runewidth" ) // Align is the type of text alignment. Actually, there are only 3 values. type Align int const ( AlignLeft Align = iota + 1 AlignCenter AlignRight ) func (a Align) String() string { switch a { case AlignCenter: return "center" case AlignLeft: return "left" case AlignRight: return "right" default: return "unknown" } } // Column is the configuration of a column. type Column struct { Header string // column name Align Align // text align MinWidth int // minimum width MaxWidth int // maximum width, it will be overrided by the global MaxWidth of the table HumanizeNumbers bool // add comma to numbers, for example 1000 -> 1,000 } // Table is the table struct. type Table struct { rows [][]string // all rows, or buffered rows of the first bufRows lines when writer is set columns []Column // configuration of each column nColumns int // the number of the header or the first row dataAdded bool // a flag to indicate that some data is added, so calling SetHeader() is not allowed hasHeader bool // a flag to say the table has a header // statistics of data in rows minWidths []int // min width of each column maxWidths []int // min width of each column widthsChecked bool // a flag to indicate whether the min/max widths of each column is checked // global options set by users align Align // text alignment minWidth int // minimum width hasGlobalMinWidth bool // minWidth is set maxWidth int // maximum width wrapDelimiter rune // delimiter for wrapping cells clipCell bool // clip cell instead of wrapping clipMark string // mark for indicating the cell if clipped humanizeNumbers bool // add comma to numbers, for example 1000 -> 1,000 // some reused datastructures, for avoiding allocate objects repeatedly slice []string // for joining cells of each row rotate [][]string // only for wrapping a row wrappedRow []*[]string // juonlyst for wrapping a row poolSlice *sync.Pool // objects pool of string slice which size is the number of columns buf bytes.Buffer // a bytes buffer style *TableStyle // output style // if the writer is set, the first bufRows rows will be used to determine // the maximum width for each cell if they are not defined with MaxWidth(). writer io.Writer hasWriter bool bufRows int // the number of rows to determine the max/min width of each column bufAll bool // when bufRows is 0, just buffer all data bufRowsDumped bool flushed bool } // New creates a new Table object. func New() *Table { t := new(Table) t.style = StylePlain return t } // -------------------------------------------------------------------------- // Style sets the output style. // If you decide to add all rows before rendering, there's no need to call this method. // If you want to stream the output, please call this method before adding any rows. func (t *Table) Style(style *TableStyle) *Table { t.style = style return t } // ErrInvalidAlign means a invalid align value is given. var ErrInvalidAlign = fmt.Errorf("stable: invalid align value") // AlignLeft sets the global text alignment as Left. func (t *Table) AlignLeft() *Table { t.align = AlignLeft return t } // AlignCenter sets the global text alignment as Center. func (t *Table) AlignCenter() *Table { t.align = AlignCenter return t } // AlignRight sets the global text alignment as Right. func (t *Table) AlignRight() *Table { t.align = AlignRight return t } // Align sets the global text alignment. // Only three values are allowed: AlignLeft, AlignCenter, AlignRight. func (t *Table) Align(align Align) (*Table, error) { switch align { case AlignLeft: t.align = AlignLeft case AlignCenter: t.align = AlignCenter case AlignRight: t.align = AlignRight default: return nil, ErrInvalidAlign } return t, nil } // MinWidth sets the global minimum cell width. func (t *Table) MinWidth(w int) *Table { if t.maxWidth > 0 && w > t.maxWidth { // even bigger than t.maxWidth t.minWidth = t.maxWidth } else { t.minWidth = w } t.hasGlobalMinWidth = true return t } // MaxWidth sets the global maximum cell width. func (t *Table) MaxWidth(w int) *Table { if t.minWidth > 0 && w < t.minWidth { // even smaller than t.minWidth t.maxWidth = t.minWidth } else { t.maxWidth = w } return t } // WrapDelimiter sets the delimiter for wrapping cell text. // The default value is space. // Note that in streaming mode (after calling SetWriter()) func (t *Table) WrapDelimiter(d rune) *Table { if t.hasWriter && t.dataAdded { return t } t.wrapDelimiter = d return t } // ClipCell sets the mark to indicate the cell is clipped. func (t *Table) ClipCell(mark string) *Table { t.clipCell = true t.clipMark = mark return t } // HumanizeNumbers makes the numbers more readable by adding commas to numbers. E.g., 1000 -> 1,000. func (t *Table) HumanizeNumbers() *Table { t.humanizeNumbers = true return t } // -------------------------------------------------------------------------- // ErrSetHeaderAfterDataAdded means that setting header is not allowed after some data being added. var ErrSetHeaderAfterDataAdded = fmt.Errorf("stable: setting header is not allowed after some data being added") // Header sets column names. func (t *Table) Header(headers []string) (*Table, error) { if t.dataAdded { return nil, ErrSetHeaderAfterDataAdded } t.columns = make([]Column, len(headers)) for i, h := range headers { t.columns[i] = Column{ Header: h, } } t.nColumns = len(headers) hasNonEmptyHeader := false for _, header := range headers { if header != "" { hasNonEmptyHeader = true break } } t.hasHeader = hasNonEmptyHeader return t, nil } // HeaderWithFormat sets column names and other configuration of the column. func (t *Table) HeaderWithFormat(headers []Column) (*Table, error) { if t.dataAdded { return nil, ErrSetHeaderAfterDataAdded } t.columns = headers t.nColumns = len(headers) hasNonEmptyHeader := false for _, header := range headers { if header.Header != "" { hasNonEmptyHeader = true break } } t.hasHeader = hasNonEmptyHeader return t, nil } // HasHeaders tell whether the table has an available header line. // It may return false even if you have called Header() or HeaderWithFormat(), // when all headers are empty strings. func (t *Table) HasHeaders() bool { return t.hasHeader } // ErrUnmatchedColumnNumber means that the column number // of the newly added row is not matched with that of previous ones. var ErrUnmatchedColumnNumber = fmt.Errorf("stable: unmatched column number") // parseRow convert a list of objects to string slice func (t *Table) parseRow(row []interface{}) ([]string, error) { _row := make([]string, len(row)) var err error var s string var humanizeNumbers bool for i, v := range row { if t.humanizeNumbers { humanizeNumbers = true } else { humanizeNumbers = t.columns[i].HumanizeNumbers } s, err = convertToString(v, humanizeNumbers) if err != nil { return nil, err } _row[i] = s } return _row, nil } // checkRow checks a row. func (t *Table) checkRow(row []interface{}) ([]string, error) { if t.hasHeader { if len(row) != t.nColumns { return nil, ErrUnmatchedColumnNumber } } else if t.columns == nil { // no header and the t.columns is nil t.columns = make([]Column, len(row)) for i := 0; i < len(row); i++ { t.columns[i] = Column{} } t.nColumns = len(row) } else { // no header if len(row) != t.nColumns { return nil, ErrUnmatchedColumnNumber } } return t.parseRow(row) } var ErrAddRowAfterFlush = fmt.Errorf("stable: calling AddRow is not allowed after calling Flush()") func (t *Table) AddRowStringSlice(row []string) error { tmp := make([]interface{}, len(row)) for i, v := range row { tmp[i] = v } return t.AddRow(tmp) } // AddRow adds a row. func (t *Table) AddRow(row []interface{}) error { if t.hasWriter && t.flushed { return ErrAddRowAfterFlush } // just adds it to buffer if !t.hasWriter || t.bufAll || len(t.rows) < t.bufRows { _row, err := t.checkRow(row) if err != nil { return err } t.rows = append(t.rows, _row) t.dataAdded = true return nil } // ------------------------------------------------ style := t.style if style == nil { // not defined in the object style = StyleGrid } buf := t.buf buf.Reset() if t.slice == nil { t.slice = make([]string, t.nColumns) } slice := t.slice lenPad2 := len(style.Padding) * 2 var wrapped bool var row2 *[]string // ------------------------------------------------ if t.bufRowsDumped { // ------------------------------------------------ // parse and check row _row, err := t.checkRow(row) if err != nil { return err } // ------------------------------------------------ // line between rows if style.LineBetweenRows.Visible() { buf.WriteString(style.LineBetweenRows.Begin) for i, M := range t.maxWidths { if t.hasGlobalMinWidth && M < t.minWidths[i] { M = t.minWidths[i] } slice[i] = strings.Repeat(style.LineBetweenRows.Hline, M+lenPad2) } buf.WriteString(strings.Join(slice, style.LineBetweenRows.Sep)) buf.WriteString(style.LineBetweenRows.End) buf.WriteString("\n") t.writer.Write(buf.Bytes()) buf.Reset() } // data row wrapped = t.formatRow(_row) if wrapped { for _, row2 = range t.wrappedRow { buf.WriteString(style.DataRow.Begin) for i, M := range t.maxWidths { if t.hasGlobalMinWidth && M < t.minWidths[i] { M = t.minWidths[i] } slice[i] = style.Padding + t.formatCell((*row2)[i], M, t.columns[i].Align) + style.Padding } buf.WriteString(strings.Join(slice, style.DataRow.Sep)) buf.WriteString(style.DataRow.End) buf.WriteString("\n") t.writer.Write(buf.Bytes()) buf.Reset() t.poolSlice.Put(row2) } } else { buf.WriteString(style.DataRow.Begin) for i, M := range t.maxWidths { if t.hasGlobalMinWidth && M < t.minWidths[i] { M = t.minWidths[i] } slice[i] = style.Padding + t.formatCell(_row[i], M, t.columns[i].Align) + style.Padding } buf.WriteString(strings.Join(slice, style.DataRow.Sep)) buf.WriteString(style.DataRow.End) buf.WriteString("\n") t.writer.Write(buf.Bytes()) buf.Reset() } return nil } // ------------------------------------------------ if len(t.rows) == t.bufRows { // determine the minWidth and maxWidth t.checkWidths() _row, err := t.checkRow(row) if err != nil { return err } t.rows = append(t.rows, _row) t.dataAdded = true // write the top line if style.LineTop.Visible() { buf.WriteString(style.LineTop.Begin) for i, M := range t.maxWidths { if t.hasGlobalMinWidth && M < t.minWidths[i] { M = t.minWidths[i] } slice[i] = strings.Repeat(style.LineTop.Hline, M+lenPad2) } buf.WriteString(strings.Join(slice, style.LineTop.Sep)) buf.WriteString(style.LineTop.End) buf.WriteString("\n") t.writer.Write(buf.Bytes()) buf.Reset() } // write the header if t.hasHeader { _row := make([]string, t.nColumns) for i, c := range t.columns { _row[i] = c.Header } wrapped = t.formatRow(_row) if wrapped { for _, row2 = range t.wrappedRow { buf.WriteString(style.HeaderRow.Begin) for i, M := range t.maxWidths { if t.hasGlobalMinWidth && M < t.minWidths[i] { M = t.minWidths[i] } slice[i] = style.Padding + t.formatCell((*row2)[i], M, t.columns[i].Align) + style.Padding } buf.WriteString(strings.Join(slice, style.HeaderRow.Sep)) buf.WriteString(style.HeaderRow.End) buf.WriteString("\n") t.writer.Write(buf.Bytes()) buf.Reset() t.poolSlice.Put(row2) } } else { buf.WriteString(style.HeaderRow.Begin) for i, M := range t.maxWidths { if t.hasGlobalMinWidth && M < t.minWidths[i] { M = t.minWidths[i] } slice[i] = style.Padding + t.formatCell(_row[i], M, t.columns[i].Align) + style.Padding } buf.WriteString(strings.Join(slice, style.HeaderRow.Sep)) buf.WriteString(style.HeaderRow.End) buf.WriteString("\n") t.writer.Write(buf.Bytes()) buf.Reset() } // line belowHeader if style.LineBelowHeader.Visible() { buf.WriteString(style.LineBelowHeader.Begin) for i, M := range t.maxWidths { if t.hasGlobalMinWidth && M < t.minWidths[i] { M = t.minWidths[i] } slice[i] = strings.Repeat(style.LineBelowHeader.Hline, M+lenPad2) } buf.WriteString(strings.Join(slice, style.LineBelowHeader.Sep)) buf.WriteString(style.LineBelowHeader.End) buf.WriteString("\n") t.writer.Write(buf.Bytes()) buf.Reset() } } // write the rows hasLineBetweenRows := style.LineBetweenRows.Visible() for j, _row := range t.rows { // line between rows if hasLineBetweenRows && j > 0 { buf.WriteString(style.LineBetweenRows.Begin) for i, M := range t.maxWidths { if t.hasGlobalMinWidth && M < t.minWidths[i] { M = t.minWidths[i] } slice[i] = strings.Repeat(style.LineBetweenRows.Hline, M+lenPad2) } buf.WriteString(strings.Join(slice, style.LineBetweenRows.Sep)) buf.WriteString(style.LineBetweenRows.End) buf.WriteString("\n") t.writer.Write(buf.Bytes()) buf.Reset() } // data row wrapped = t.formatRow(_row) if wrapped { for _, row2 = range t.wrappedRow { buf.WriteString(style.DataRow.Begin) for i, M := range t.maxWidths { if t.hasGlobalMinWidth && M < t.minWidths[i] { M = t.minWidths[i] } slice[i] = style.Padding + t.formatCell((*row2)[i], M, t.columns[i].Align) + style.Padding } buf.WriteString(strings.Join(slice, style.DataRow.Sep)) buf.WriteString(style.DataRow.End) buf.WriteString("\n") t.writer.Write(buf.Bytes()) buf.Reset() t.poolSlice.Put(row2) } } else { buf.WriteString(style.DataRow.Begin) for i, M := range t.maxWidths { if t.hasGlobalMinWidth && M < t.minWidths[i] { M = t.minWidths[i] } slice[i] = style.Padding + t.formatCell(_row[i], M, t.columns[i].Align) + style.Padding } buf.WriteString(strings.Join(slice, style.DataRow.Sep)) buf.WriteString(style.DataRow.End) buf.WriteString("\n") t.writer.Write(buf.Bytes()) buf.Reset() } } t.bufRowsDumped = true } return nil } // formatRow wraps or clips cells. // the returned value indicate if any cells are wrapped func (t *Table) formatRow(row []string) bool { // ------------------------------------------------------------- // initialize some data structures if t.rotate == nil { t.rotate = make([][]string, t.nColumns) for i := range t.rotate { t.rotate[i] = make([]string, 0, 8) } } else { for i := range t.rotate { t.rotate[i] = t.rotate[i][:0] } } if t.wrappedRow == nil { t.wrappedRow = make([]*[]string, 0, 8) } else { t.wrappedRow = t.wrappedRow[:0] } if t.poolSlice == nil { t.poolSlice = &sync.Pool{New: func() interface{} { tmp := make([]string, t.nColumns) return &tmp }} } if t.wrapDelimiter == 0 { t.wrapDelimiter = ' ' } // ------------------------------------------------------------- var needWrap = false for i, c := range row { if len(c) > t.maxWidths[i] { needWrap = true } } if !needWrap { return false } // ------------------------------------------------------------- var maxWidth int var w int var r rune var i, j int var cell string var workingLine string var spacePos charPos var lastPos charPos lenClipMark := len(t.clipMark) for i, cell = range row { maxWidth = t.maxWidths[i] if maxWidth < t.minWidth { maxWidth = t.minWidth } if len(cell) <= maxWidth { t.rotate[i] = append(t.rotate[i], cell) continue } // --------------------------------------------------- // clip if t.clipCell && len(cell) > maxWidth { if lenClipMark > maxWidth { t.clipMark = "" lenClipMark = len(t.clipMark) } t.rotate[i] = append(t.rotate[i], runewidth.Truncate(cell, maxWidth, t.clipMark)) continue } // --------------------------------------------------- // wrap // modify from https://github.com/donatj/wordwrap workingLine = "" spacePos.pos = 0 spacePos.size = 0 lastPos.pos = 0 lastPos.size = 0 for _, r = range cell { w = utf8.RuneLen(r) workingLine += string(r) if r == t.wrapDelimiter { spacePos.pos = len(workingLine) spacePos.size = w } if len(workingLine) >= maxWidth { if spacePos.size > 0 { t.rotate[i] = append(t.rotate[i], workingLine[0:spacePos.pos]) workingLine = workingLine[spacePos.pos:] } else { if len(workingLine) > maxWidth { t.rotate[i] = append(t.rotate[i], workingLine[0:lastPos.pos]) workingLine = workingLine[lastPos.pos:] } else { t.rotate[i] = append(t.rotate[i], workingLine) workingLine = "" } } if len(t.rotate[i][len(t.rotate[i])-1]) > maxWidth { panic("attempted to cut character") } spacePos.pos = 0 spacePos.size = 0 } lastPos.pos = len(workingLine) lastPos.size = w } if workingLine != "" { t.rotate[i] = append(t.rotate[i], workingLine) } } var maxRow int for _, tmp := range t.rotate { if len(tmp) > maxRow { maxRow = len(tmp) } } var row2 *[]string for j = 0; j < maxRow; j++ { row2 = t.poolSlice.Get().(*[]string) for i = 0; i < t.nColumns; i++ { if j+1 > len(t.rotate[i]) { (*row2)[i] = "" } else { (*row2)[i] = t.rotate[i][j] } } t.wrappedRow = append(t.wrappedRow, row2) } return true } type charPos struct { pos, size int } // formatCell formats a cell with given width and text alignment. func (t *Table) formatCell(text string, width int, align Align) string { a := align if t.align > 0 { // global align a = t.align } lenText := runewidth.StringWidth(text) // here, width need to be >= len(text) if width-lenText < 0 { panic("wrapping/clipping method error, please contact the author") } // replace tabs with spaces if strings.Contains(text, "\t") { lenText += strings.Count(text, "\t") text = strings.ReplaceAll(text, "\t", " ") } var out string switch a { case AlignCenter: n := (width - lenText) / 2 out = strings.Repeat(" ", n) + text + strings.Repeat(" ", width-lenText-n) case AlignLeft: out = text + strings.Repeat(" ", width-lenText) case AlignRight: out = strings.Repeat(" ", width-lenText) + text default: out = text + strings.Repeat(" ", width-lenText) } return out } // Render render all data with give style. func (t *Table) Render(style *TableStyle) []byte { if style == nil { // the argument not given style = t.style } if style == nil { // not defined in the object style = StyleGrid } buf := t.buf buf.Reset() if t.slice == nil { t.slice = make([]string, t.nColumns) } slice := t.slice lenPad2 := len(style.Padding) * 2 var wrapped bool // determine the minWidth and maxWidth t.checkWidths() // write the top line if style.LineTop.Visible() { buf.WriteString(style.LineTop.Begin) for i, M := range t.maxWidths { if t.hasGlobalMinWidth && M < t.minWidths[i] { M = t.minWidths[i] } slice[i] = strings.Repeat(style.LineTop.Hline, M+lenPad2) } buf.WriteString(strings.Join(slice, style.LineTop.Sep)) buf.WriteString(style.LineTop.End) buf.WriteString("\n") } // write the header var row2 *[]string if t.hasHeader { _row := make([]string, t.nColumns) for i, c := range t.columns { _row[i] = c.Header } wrapped = t.formatRow(_row) if wrapped { for _, row2 = range t.wrappedRow { buf.WriteString(style.HeaderRow.Begin) for i, M := range t.maxWidths { if t.hasGlobalMinWidth && M < t.minWidths[i] { M = t.minWidths[i] } slice[i] = style.Padding + t.formatCell((*row2)[i], M, t.columns[i].Align) + style.Padding } buf.WriteString(strings.Join(slice, style.HeaderRow.Sep)) buf.WriteString(style.HeaderRow.End) buf.WriteString("\n") t.poolSlice.Put(row2) } } else { buf.WriteString(style.HeaderRow.Begin) for i, M := range t.maxWidths { if t.hasGlobalMinWidth && M < t.minWidths[i] { M = t.minWidths[i] } slice[i] = style.Padding + t.formatCell(_row[i], M, t.columns[i].Align) + style.Padding } buf.WriteString(strings.Join(slice, style.HeaderRow.Sep)) buf.WriteString(style.HeaderRow.End) buf.WriteString("\n") } // line belowHeader if style.LineBelowHeader.Visible() { buf.WriteString(style.LineBelowHeader.Begin) for i, M := range t.maxWidths { if t.hasGlobalMinWidth && M < t.minWidths[i] { M = t.minWidths[i] } slice[i] = strings.Repeat(style.LineBelowHeader.Hline, M+lenPad2) } buf.WriteString(strings.Join(slice, style.LineBelowHeader.Sep)) buf.WriteString(style.LineBelowHeader.End) buf.WriteString("\n") } } // write the rows hasLineBetweenRows := style.LineBetweenRows.Visible() for j, _row := range t.rows { // line between rows if hasLineBetweenRows && j > 0 { buf.WriteString(style.LineBetweenRows.Begin) for i, M := range t.maxWidths { if t.hasGlobalMinWidth && M < t.minWidths[i] { M = t.minWidths[i] } slice[i] = strings.Repeat(style.LineBetweenRows.Hline, M+lenPad2) } buf.WriteString(strings.Join(slice, style.LineBetweenRows.Sep)) buf.WriteString(style.LineBetweenRows.End) buf.WriteString("\n") } // data row wrapped = t.formatRow(_row) if wrapped { for _, row2 = range t.wrappedRow { buf.WriteString(style.DataRow.Begin) for i, M := range t.maxWidths { if t.hasGlobalMinWidth && M < t.minWidths[i] { M = t.minWidths[i] } slice[i] = style.Padding + t.formatCell((*row2)[i], M, t.columns[i].Align) + style.Padding } buf.WriteString(strings.Join(slice, style.DataRow.Sep)) buf.WriteString(style.DataRow.End) buf.WriteString("\n") t.poolSlice.Put(row2) } } else { buf.WriteString(style.DataRow.Begin) for i, M := range t.maxWidths { if t.hasGlobalMinWidth && M < t.minWidths[i] { M = t.minWidths[i] } slice[i] = style.Padding + t.formatCell(_row[i], M, t.columns[i].Align) + style.Padding } buf.WriteString(strings.Join(slice, style.DataRow.Sep)) buf.WriteString(style.DataRow.End) buf.WriteString("\n") } } // bottom line if style.LineBottom.Visible() { buf.WriteString(style.LineBottom.Begin) for i, M := range t.maxWidths { if t.hasGlobalMinWidth && M < t.minWidths[i] { M = t.minWidths[i] } slice[i] = strings.Repeat(style.LineBottom.Hline, M+lenPad2) } buf.WriteString(strings.Join(slice, style.LineBottom.Sep)) buf.WriteString(style.LineBottom.End) buf.WriteString("\n") } return buf.Bytes() } // ErrNoDataAdded means not data is added. Not used. var ErrNoDataAdded = fmt.Errorf("stable: no data added") // checkWidths determine the minimum and maximum widths of each column. func (t *Table) checkWidths() error { // if t.hasHeader && !t.dataAdded { // return ErrNoDataAdded // } t.minWidths = make([]int, t.nColumns) for i := range t.minWidths { t.minWidths[i] = math.MaxInt } t.maxWidths = make([]int, t.nColumns) var i, l int var c Column if t.hasHeader { for i, c = range t.columns { l = len(c.Header) if l > t.maxWidths[i] { t.maxWidths[i] = l } if l < t.minWidths[i] { t.minWidths[i] = l } } } var v string for _, row := range t.rows { for i, v = range row { l = len(v) if l > t.maxWidths[i] { t.maxWidths[i] = l } if l < t.minWidths[i] { t.minWidths[i] = l } } } for i, c := range t.columns { if c.MaxWidth > 0 && c.MaxWidth < t.maxWidths[i] { // use user defined threshold t.maxWidths[i] = c.MaxWidth } if t.maxWidth > 0 && t.maxWidth < t.maxWidths[i] { // use user defined global threshold t.maxWidths[i] = t.maxWidth } if t.maxWidths[i] < 1 { t.maxWidths[i] = 1 } if c.MinWidth > 0 && c.MinWidth > t.minWidths[i] { // use user defined threshold t.minWidths[i] = c.MinWidth } if t.minWidth > 0 { // use user defined global threshold t.minWidths[i] = t.minWidth } } t.widthsChecked = true // fmt.Println(t.minWidths) // fmt.Println(t.maxWidths) return nil } // -------------------------------------------------------------------------- // ErrWriterRepeatedlySet means that the writer is repeatedly set. var ErrWriterRepeatedlySet = fmt.Errorf("stable: writer repeatedly set") // Writer sets a writer for render the table. The first bufRows rows will // be used to determine the maximum width for each cell if they are not defined // with MaxWidth(). bufRows should be in range of [1,1M]. // If bufRows is 0, it keeps all data in buffer. // So a newly added row (Addrow()) is formatted and written to the configured writer immediately. // It is memory-effective for a large number of rows. // And it is helpful to pipe the data in shell. // Do not forget to call Flush() after adding all rows. func (t *Table) Writer(w io.Writer, bufRows uint) error { if t.hasWriter { return ErrWriterRepeatedlySet } t.writer = w t.hasWriter = true if bufRows == 0 { t.bufAll = true bufRows = 1024 } else if bufRows > 1<<20 { bufRows = 1 << 20 } t.rows = make([][]string, 0, bufRows) t.bufRows = int(bufRows) return nil } // Flush dumps the remaining data. func (t *Table) Flush() { t.flushed = true style := t.style if style == nil { // not defined in the object style = StyleGrid } buf := t.buf buf.Reset() if t.slice == nil { t.slice = make([]string, t.nColumns) } slice := t.slice lenPad2 := len(style.Padding) * 2 // ------------------------------------------------ // only need to append the bottown line if t.bufRowsDumped { // bottom line if style.LineBottom.Visible() { buf.WriteString(style.LineBottom.Begin) for i, M := range t.maxWidths { if t.hasGlobalMinWidth && M < t.minWidths[i] { M = t.minWidths[i] } slice[i] = strings.Repeat(style.LineBottom.Hline, M+lenPad2) } buf.WriteString(strings.Join(slice, style.LineBottom.Sep)) buf.WriteString(style.LineBottom.End) buf.WriteString("\n") t.writer.Write(buf.Bytes()) buf.Reset() } return } // ------------------------------------------------ // dump all buffered line t.writer.Write(t.Render(style)) buf.Reset() return } stable-0.1.7/table_test.go000066400000000000000000000116171454160570700154650ustar00rootroot00000000000000// Copyright © 2023 Wei Shen // // 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. package stable import ( "fmt" "os" "testing" ) func TestBasic(t *testing.T) { tbl := New().HumanizeNumbers().MaxWidth(40) tbl.Header([]string{ "id", "name", "sentence", }) tbl.AddRow([]interface{}{100, "Donec Vitae", "Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse."}) tbl.AddRow([]interface{}{2000, "Quaerat Voluptatem", "At vero eos et accusamus et iusto odio."}) tbl.AddRow([]interface{}{250, "with tab", "<-left cell has one tab."}) tbl.AddRow([]interface{}{250, "with tab", "<-left cell has two tabs."}) tbl.AddRow([]interface{}{3000000, "Aliquam lorem", "Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero."}) // fmt.Printf("%s\n", tbl.Render(StyleGrid)) for _, style := range []*TableStyle{ StylePlain, StyleSimple, StyleThreeLine, StyleGrid, StyleLight, StyleBold, StyleDouble, } { fmt.Printf("style: %s\n%s\n", style.Name, tbl.Render(style)) } } func TestUnicode(t *testing.T) { tbl := New().HumanizeNumbers().MaxWidth(20) //.ClipCell("...") tbl.Header([]string{ "id", "name", "sentence", }) tbl.AddRow([]interface{}{100, "Wei Shen", "How are you?"}) tbl.AddRow([]interface{}{1000, "沈 伟", "I'm fine, thank you. And you?"}) tbl.AddRow([]interface{}{1000, "沈 伟", "There's one tab between the two words"}) tbl.AddRow([]interface{}{100000, "沈伟", "谢谢,我很好,你呢?"}) fmt.Printf("%s\n", tbl.Render(StyleGrid)) } func TestCustomColumns(t *testing.T) { tbl := New() tbl.HeaderWithFormat([]Column{ {Header: "number", MinWidth: 10, MaxWidth: 15, HumanizeNumbers: true, Align: AlignRight}, {Header: "name", MinWidth: 10, MaxWidth: 15, Align: AlignCenter}, {Header: "sentence", MaxWidth: 40, Align: AlignLeft}, }) tbl.AddRow([]interface{}{100, "Donec Vitae", "Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse."}) tbl.AddRow([]interface{}{2000, "Quaerat Voluptatem", "At vero eos et accusamus et iusto odio."}) tbl.AddRow([]interface{}{3000000, "Aliquam lorem", "Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero."}) fmt.Printf("%s\n", tbl.Render(StyleGrid)) } func TestStreaming(t *testing.T) { tbl := New().MinWidth(10) // write to stdout, and determine the max width according to the first row tbl.Writer(os.Stdout, 1) tbl.Style(StyleGrid) tbl.Header([]string{ "number", "name", "sentence", }) // when a new row is added, it writes to stdout immediately. tbl.AddRow([]interface{}{100, "Donec Vitae", "Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse."}) tbl.AddRow([]interface{}{2000, "Quaerat Voluptatem", "At vero eos et accusamus et iusto odio."}) tbl.AddRow([]interface{}{3000000, "Aliquam lorem", "Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero."}) // flush the remaining data tbl.Flush() } func TestTaxonomicLineages(t *testing.T) { tbl := New() tbl.Header([]string{ "taxid", "name", "complete lineage", }) tbl.AddRow([]interface{}{ 9606, "Homo sapiens", "cellular organisms;Eukaryota;Opisthokonta;Metazoa;Eumetazoa;Bilateria;Deuterostomia;Chordata;Craniata;Vertebrata;Gnathostomata;Teleostomi;Euteleostomi;Sarcopterygii;Dipnotetrapodomorpha;Tetrapoda;Amniota;Mammalia;Theria;Eutheria;Boreoeutheria;Euarchontoglires;Primates;Haplorrhini;Simiiformes;Catarrhini;Hominoidea;Hominidae;Homininae;Homo;Homo sapiens", }) tbl.AddRow([]interface{}{ 562, "Escherichia coli", "cellular organisms;Bacteria;Pseudomonadota;Gammaproteobacteria;Enterobacterales;Enterobacteriaceae;Escherichia;Escherichia coli", }) fmt.Printf("%s\n", tbl.WrapDelimiter(';').AlignLeft().MaxWidth(50).Render(StyleGrid)) } stable-0.1.7/util.go000066400000000000000000000065351454160570700143170ustar00rootroot00000000000000// Copyright © 2023 Wei Shen // // 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. package stable import ( "errors" "fmt" "strconv" "github.com/dustin/go-humanize" ) // from https://github.com/tatsushid/go-prettytable func convertToString(v interface{}, addComma bool) (string, error) { if addComma { switch vv := v.(type) { case fmt.Stringer: return vv.String(), nil case int: return humanize.Comma(int64(vv)), nil case int8: return humanize.Comma(int64(vv)), nil case int16: return humanize.Comma(int64(vv)), nil case int32: return humanize.Comma(int64(vv)), nil case int64: return humanize.Comma(vv), nil case uint: return humanize.Comma(int64(vv)), nil case uint8: return humanize.Comma(int64(vv)), nil case uint16: return humanize.Comma(int64(vv)), nil case uint32: return humanize.Comma(int64(vv)), nil case uint64: return humanize.Comma(int64(vv)), nil case float32: return humanize.Commaf(float64(vv)), nil case float64: return humanize.Commaf(float64(vv)), nil case bool: return strconv.FormatBool(vv), nil case string: return vv, nil case []byte: return string(vv), nil case []rune: return string(vv), nil default: return "", errors.New("can't convert the value") } } switch vv := v.(type) { case fmt.Stringer: return vv.String(), nil case int: return strconv.FormatInt(int64(vv), 10), nil case int8: return strconv.FormatInt(int64(vv), 10), nil case int16: return strconv.FormatInt(int64(vv), 10), nil case int32: return strconv.FormatInt(int64(vv), 10), nil case int64: return strconv.FormatInt(vv, 10), nil case uint: return strconv.FormatUint(uint64(vv), 10), nil case uint8: return strconv.FormatUint(uint64(vv), 10), nil case uint16: return strconv.FormatUint(uint64(vv), 10), nil case uint32: return strconv.FormatUint(uint64(vv), 10), nil case uint64: return strconv.FormatUint(vv, 10), nil case float32: return strconv.FormatFloat(float64(vv), 'g', -1, 32), nil case float64: return strconv.FormatFloat(vv, 'g', -1, 64), nil case bool: return strconv.FormatBool(vv), nil case string: return vv, nil case []byte: return string(vv), nil case []rune: return string(vv), nil default: return "", errors.New("can't convert the value") } } func max(a, b int) int { if a > b { return a } return b }