prettytable.go 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package prettytable
  15. import (
  16. "bytes"
  17. "strings"
  18. "unicode"
  19. )
  20. type AlignmentType uint8
  21. const (
  22. AlignLeft AlignmentType = iota
  23. AlignCenter
  24. AlignRight
  25. )
  26. const TabWidth = 8
  27. type ptColumn struct {
  28. Title string
  29. Align AlignmentType
  30. }
  31. type PrettyTable struct {
  32. columns []ptColumn
  33. maxLineWidth int
  34. tryTermWidth bool
  35. }
  36. func NewPrettyTable(fields []string) *PrettyTable {
  37. var pt = PrettyTable{
  38. tryTermWidth: true,
  39. }
  40. for _, field := range fields {
  41. col := ptColumn{Title: field, Align: AlignLeft}
  42. pt.columns = append(pt.columns, col)
  43. }
  44. return &pt
  45. }
  46. func NewPrettyTableWithTryTermWidth(fields []string, tryTermWidth bool) *PrettyTable {
  47. pt := NewPrettyTable(fields)
  48. pt.tryTermWidth = tryTermWidth
  49. return pt
  50. }
  51. func rowLine(buf *bytes.Buffer, widths []int) {
  52. buf.WriteByte('+')
  53. for _, w := range widths {
  54. buf.WriteByte('-')
  55. for i := 0; i < w; i++ {
  56. buf.WriteByte('-')
  57. }
  58. buf.WriteByte('-')
  59. buf.WriteByte('+')
  60. }
  61. buf.WriteByte('\n')
  62. }
  63. func textCell(buf *bytes.Buffer, col ptColumn, width int, nthCol int, prevWidth int) {
  64. var padLeft, padRight int
  65. celWidth := cellDisplayWidth(col.Title, nthCol, prevWidth)
  66. switch col.Align {
  67. case AlignLeft:
  68. padLeft = 0
  69. padRight = width - celWidth
  70. case AlignRight:
  71. padLeft = width - celWidth
  72. padRight = 0
  73. default: //case AlignCenter:
  74. padLeft = (width - celWidth) / 2
  75. padRight = width - padLeft - celWidth
  76. }
  77. for i := 0; i < padLeft; i++ {
  78. buf.WriteByte(' ')
  79. }
  80. buf.WriteString(col.Title)
  81. for i := 0; i < padRight; i++ {
  82. buf.WriteByte(' ')
  83. }
  84. }
  85. func textLine(buf *bytes.Buffer, columns []ptColumn, widths []int) {
  86. nlines := 0
  87. splitted := make([][]string, len(columns))
  88. for i, column := range columns {
  89. title := strings.TrimRight(column.Title, "\n")
  90. s := strings.Split(title, "\n")
  91. splitted[i] = s
  92. if nlines < len(s) {
  93. nlines = len(s)
  94. }
  95. }
  96. for j := 0; j < nlines; j++ {
  97. buf.WriteByte('|')
  98. widthAcc := 0
  99. for i, w := range widths {
  100. buf.WriteByte(' ')
  101. var text string
  102. if j < len(splitted[i]) {
  103. text = splitted[i][j]
  104. }
  105. c := ptColumn{
  106. Title: text,
  107. Align: columns[i].Align,
  108. }
  109. textCell(buf, c, w, i, widthAcc)
  110. widthAcc += w
  111. buf.WriteByte(' ')
  112. buf.WriteByte('|')
  113. }
  114. buf.WriteByte('\n')
  115. }
  116. }
  117. func runeDisplayWidth(r rune) int {
  118. const puncts = "。,;:()、?《》"
  119. if unicode.Is(unicode.Han, r) {
  120. return 2
  121. }
  122. if strings.ContainsRune(puncts, r) {
  123. return 2
  124. }
  125. return 1
  126. }
  127. // cellDisplayWidth returns display width of the cell content (excluding bars
  128. // and paddings) when printed as the nthCol.
  129. //
  130. // nthCol is 0-based
  131. //
  132. // prevWidth is the total display width (as return by this same func) of
  133. // previous cells in the same line. It will be used to calculate width of tab
  134. // characters
  135. func cellDisplayWidth(cell string, nthCol int, prevWidth int) int {
  136. // `| ` at the front and ` | ` in the middle
  137. sX := prevWidth + nthCol*3 + 2
  138. width := 0
  139. lines := strings.Split(cell, "\n")
  140. for _, line := range lines {
  141. displayWidth := 0
  142. x := sX
  143. for _, c := range line {
  144. incr := 0
  145. if c != '\t' {
  146. incr = runeDisplayWidth(c)
  147. } else {
  148. // terminal with have the char TabWidth aligned
  149. incr = TabWidth - (x & (TabWidth - 1))
  150. }
  151. displayWidth += incr
  152. x += incr
  153. }
  154. if width < displayWidth {
  155. width = displayWidth
  156. }
  157. }
  158. return width
  159. }
  160. func wrapCell(cell string, nthCol int, prevWidth int, newWidth int) string {
  161. sX := prevWidth + nthCol*3 + 2
  162. var (
  163. wrapped = false
  164. newCell = ""
  165. cline = make([]rune, 0, newWidth+1)
  166. cw = 0
  167. cx = sX
  168. )
  169. for _, c := range cell {
  170. var incr int
  171. if c == '\n' {
  172. cline = append(cline, c)
  173. newCell += string(cline)
  174. cline = cline[:0]
  175. cw = 0
  176. cx = sX
  177. continue
  178. } else if c != '\t' {
  179. incr = runeDisplayWidth(c)
  180. } else {
  181. // terminal with have the char TabWidth aligned
  182. incr = TabWidth - (cx & (TabWidth - 1))
  183. }
  184. t := cw + incr
  185. if t < newWidth {
  186. cline = append(cline, c)
  187. cw += incr
  188. cx += incr
  189. } else if t == newWidth {
  190. cline = append(cline, c, '\n')
  191. newCell += string(cline)
  192. cline = cline[:0]
  193. cw = 0
  194. cx = sX
  195. if !wrapped {
  196. wrapped = true
  197. }
  198. } else {
  199. if len(cline) == 0 {
  200. newCell += string(c) + "\n"
  201. } else {
  202. newCell += string(cline) + "\n"
  203. cline = cline[:1]
  204. cline[0] = c
  205. }
  206. cw = 0
  207. cx = sX
  208. if !wrapped {
  209. wrapped = true
  210. }
  211. }
  212. }
  213. if len(cline) > 0 {
  214. newCell += string(cline)
  215. } else if !wrapped && newCell != "" {
  216. l := len(newCell) - 1
  217. c := newCell[l]
  218. if c == '\n' {
  219. newCell = newCell[:l]
  220. }
  221. }
  222. return newCell
  223. }
  224. func (this *PrettyTable) MaxLineWidth(w int) {
  225. this.maxLineWidth = w
  226. }
  227. func (this *PrettyTable) hasMaxLineWidth() (int, bool) {
  228. if this.maxLineWidth > 0 {
  229. return this.maxLineWidth, true
  230. }
  231. if this.tryTermWidth {
  232. w, err := termWidth()
  233. if err != nil {
  234. return 0, false
  235. }
  236. return w, true
  237. }
  238. return 0, false
  239. }
  240. func (this *PrettyTable) GetString(fields [][]string) string {
  241. if len(fields) == 0 {
  242. return ""
  243. }
  244. var (
  245. widths = make([]int, len(this.columns))
  246. widthAcc = 0
  247. )
  248. {
  249. // width of title columns
  250. widthAcc = 0
  251. for i, c := range this.columns {
  252. widths[i] = cellDisplayWidth(c.Title, i, widthAcc)
  253. widthAcc += widths[i]
  254. }
  255. }
  256. {
  257. // width of content columns
  258. widthAcc = 0
  259. for col := 0; ; col++ {
  260. cont := false
  261. colWidth := 0
  262. for _, line := range fields {
  263. if col < len(line) {
  264. cont = true
  265. cw := cellDisplayWidth(line[col], col, widthAcc)
  266. if colWidth < cw {
  267. colWidth = cw
  268. }
  269. }
  270. }
  271. if cont {
  272. if widths[col] < colWidth {
  273. widths[col] = colWidth
  274. }
  275. widthAcc += widths[col]
  276. } else {
  277. break
  278. }
  279. }
  280. }
  281. if maxLineWidth, ok := this.hasMaxLineWidth(); ok {
  282. nCols := len(this.columns)
  283. maxContWidth := maxLineWidth - 3*nCols - 1
  284. if maxContWidth < nCols {
  285. // ensure at least 1 ascii char print
  286. maxContWidth = nCols
  287. }
  288. if widthAcc > maxContWidth {
  289. assured := maxContWidth / len(this.columns)
  290. rem := maxContWidth
  291. nwrap := 0
  292. for _, w := range widths {
  293. if w <= assured {
  294. rem -= w
  295. } else {
  296. nwrap += 1
  297. }
  298. }
  299. // break long lines
  300. widthAcc := 0
  301. wrapped := rem / nwrap
  302. wrappedRem := rem % nwrap
  303. for col, w := range widths {
  304. if w > assured {
  305. newW := wrapped
  306. if wrappedRem > 0 {
  307. newW += 1
  308. wrappedRem -= 1
  309. }
  310. this.columns[col].Title = wrapCell(this.columns[col].Title, col, widthAcc, newW)
  311. for _, line := range fields {
  312. line[col] = wrapCell(line[col], col, widthAcc, newW)
  313. }
  314. widths[col] = newW // later tabs may need correction
  315. widthAcc += newW
  316. } else {
  317. widthAcc += w
  318. }
  319. }
  320. }
  321. }
  322. var columns = make([]ptColumn, len(this.columns))
  323. var buf bytes.Buffer
  324. rowLine(&buf, widths)
  325. for i := 0; i < len(columns); i++ {
  326. columns[i].Title = this.columns[i].Title
  327. columns[i].Align = AlignCenter
  328. }
  329. textLine(&buf, columns, widths)
  330. rowLine(&buf, widths)
  331. for _, line := range fields {
  332. for i := 0; i < len(line); i++ {
  333. columns[i].Title = line[i]
  334. columns[i].Align = this.columns[i].Align
  335. }
  336. textLine(&buf, columns, widths)
  337. }
  338. rowLine(&buf, widths)
  339. return buf.String()
  340. }