pkg.re/essentialkaos/ek.10@v12.41.0+incompatible/fmtutil/table/table.go (about) 1 // Package table contains methods and structs for rendering data in tabular format 2 package table 3 4 // ////////////////////////////////////////////////////////////////////////////////// // 5 // // 6 // Copyright (c) 2022 ESSENTIAL KAOS // 7 // Apache License, Version 2.0 <https://www.apache.org/licenses/LICENSE-2.0> // 8 // // 9 // ////////////////////////////////////////////////////////////////////////////////// // 10 11 import ( 12 "fmt" 13 "strings" 14 15 "pkg.re/essentialkaos/ek.v12/fmtc" 16 "pkg.re/essentialkaos/ek.v12/mathutil" 17 "pkg.re/essentialkaos/ek.v12/strutil" 18 "pkg.re/essentialkaos/ek.v12/terminal/window" 19 ) 20 21 // ////////////////////////////////////////////////////////////////////////////////// // 22 23 // Column alignment flags 24 const ( 25 ALIGN_LEFT uint8 = 0 26 ALIGN_CENTER = 1 27 ALIGN_RIGHT = 2 28 ) 29 30 // ////////////////////////////////////////////////////////////////////////////////// // 31 32 // Table is struct which can be used for table rendering 33 type Table struct { 34 Sizes []int // Custom columns sizes 35 Headers []string // Slice with headers 36 Alignment []uint8 // Columns alignment 37 38 // Slice with data 39 data [][]string 40 41 // Separator cache 42 separator string 43 44 // Flag will be set if header is rendered 45 headerShown bool 46 47 // Slice with auto calculated sizes 48 columnSizes []int 49 } 50 51 // ////////////////////////////////////////////////////////////////////////////////// // 52 53 // HeaderCapitalize is flag for capitalizing headers by default 54 var HeaderCapitalize = false 55 56 // HeaderColorTag is fmtc tag used for headers by default for all tables 57 var HeaderColorTag = "{*}" 58 59 // SeparatorSymbol used for separator generation 60 var SeparatorSymbol = "-" 61 62 // ColumnSeparatorSymbol is column separator symbol 63 var ColumnSeparatorSymbol = "|" 64 65 // MaxWidth is a maximum table width 66 var MaxWidth = 0 67 68 // ////////////////////////////////////////////////////////////////////////////////// // 69 70 // NewTable creates new table struct 71 func NewTable(headers ...string) *Table { 72 return &Table{Headers: headers} 73 } 74 75 // ////////////////////////////////////////////////////////////////////////////////// // 76 77 // SetHeaders sets headers 78 func (t *Table) SetHeaders(headers ...string) *Table { 79 if t == nil { 80 return nil 81 } 82 83 t.Headers = headers 84 85 return t 86 } 87 88 // SetSizes sets size of columns 89 func (t *Table) SetSizes(sizes ...int) *Table { 90 if t == nil { 91 return nil 92 } 93 94 t.Sizes = sizes 95 96 return t 97 } 98 99 // SetAlignments sets aligment of columns 100 func (t *Table) SetAlignments(align ...uint8) *Table { 101 if t == nil { 102 return nil 103 } 104 105 t.Alignment = align 106 107 return t 108 } 109 110 // Add adds given data to stack 111 func (t *Table) Add(data ...interface{}) *Table { 112 if t == nil { 113 return nil 114 } 115 116 if len(data) == 0 { 117 return t 118 } 119 120 t.data = append(t.data, convertSlice(data)) 121 122 return t 123 } 124 125 // Print renders given data 126 func (t *Table) Print(data ...interface{}) *Table { 127 if t == nil { 128 return nil 129 } 130 131 if len(data) == 0 { 132 return t 133 } 134 135 if len(t.Headers) == 0 && len(t.Sizes) == 0 { 136 setColumnsSizes(t, len(data)) 137 } 138 139 prepareRender(t) 140 renderRowData(t, convertSlice(data), len(t.columnSizes)) 141 142 return t 143 } 144 145 // HasData returns true if table stack has some data 146 func (t *Table) HasData() bool { 147 return t != nil && len(t.data) != 0 148 } 149 150 // Separator renders separator 151 func (t *Table) Separator() *Table { 152 if t == nil { 153 return nil 154 } 155 156 if t.separator == "" { 157 t.separator = strings.Repeat(SeparatorSymbol, getSeparatorSize(t)) 158 } 159 160 fmtc.Println("{s}" + t.separator + "{!}") 161 162 return t 163 } 164 165 // RenderHeaders renders headers 166 func (t *Table) RenderHeaders() { 167 if len(t.columnSizes) == 0 { 168 calculateColumnSizes(t) 169 } 170 171 renderHeaders(t) 172 } 173 174 // Render renders data 175 func (t *Table) Render() *Table { 176 if t == nil { 177 return nil 178 } 179 180 // Nothing to render 181 if len(t.Headers) == 0 && len(t.data) == 0 { 182 return t 183 } 184 185 prepareRender(t) 186 187 if len(t.Headers) == 0 { 188 t.Separator() 189 } 190 191 if t.data != nil { 192 renderData(t) 193 } 194 195 // Remove data after rendering 196 t.separator = "" 197 t.data = nil 198 t.columnSizes = nil 199 t.headerShown = false 200 201 return t 202 } 203 204 // ////////////////////////////////////////////////////////////////////////////////// // 205 206 // prepareRender prepare table for render 207 func prepareRender(t *Table) { 208 if len(t.columnSizes) == 0 { 209 calculateColumnSizes(t) 210 } 211 212 if !t.headerShown { 213 renderHeaders(t) 214 } 215 } 216 217 // renderHeaders render headers 218 func renderHeaders(t *Table) { 219 t.headerShown = true 220 221 if len(t.Headers) == 0 { 222 return 223 } 224 225 t.Separator() 226 227 totalHeaders := len(t.Headers) 228 totalColumns := len(t.columnSizes) 229 230 var headerText string 231 232 for columnIndex, columnSize := range t.columnSizes { 233 if columnIndex >= totalHeaders { 234 headerText = strings.Repeat(" ", columnSize) 235 } else { 236 headerText = t.Headers[columnIndex] 237 } 238 239 if HeaderCapitalize { 240 headerText = strings.ToUpper(headerText) 241 } 242 243 fmtc.Printf(" " + HeaderColorTag + formatText(headerText, t.columnSizes[columnIndex], getAlignment(t, columnIndex)) + "{!} ") 244 245 if columnIndex+1 != totalColumns { 246 fmtc.Printf("{s}%s{!}", ColumnSeparatorSymbol) 247 } else { 248 fmtc.NewLine() 249 } 250 } 251 252 t.Separator() 253 } 254 255 // renderData render table data 256 func renderData(t *Table) { 257 totalColumns := len(t.columnSizes) 258 259 for _, rowData := range t.data { 260 renderRowData(t, rowData, totalColumns) 261 } 262 263 t.Separator() 264 } 265 266 // renderRowData render data in row 267 func renderRowData(t *Table, rowData []string, totalColumns int) { 268 for columnIndex, columnData := range rowData { 269 if columnIndex == totalColumns { 270 break 271 } 272 273 if strutil.Len(fmtc.Clean(columnData)) > t.columnSizes[columnIndex] { 274 fmtc.Print(" " + strutil.Ellipsis(columnData, t.columnSizes[columnIndex]) + " ") 275 } else { 276 if columnIndex+1 == totalColumns && getAlignment(t, columnIndex) == ALIGN_LEFT { 277 fmtc.Print(" " + formatText(columnData, -1, ALIGN_LEFT)) 278 } else { 279 fmtc.Print(" " + formatText(columnData, t.columnSizes[columnIndex], getAlignment(t, columnIndex)) + " ") 280 } 281 } 282 283 if columnIndex+1 != totalColumns { 284 fmtc.Printf("{s}%s{!}", ColumnSeparatorSymbol) 285 } 286 } 287 288 fmtc.NewLine() 289 } 290 291 // convertSlice convert slice with interface{} to slice with strings 292 func convertSlice(data []interface{}) []string { 293 var result []string 294 295 for _, item := range data { 296 result = append(result, fmt.Sprintf("%v", item)) 297 } 298 299 return result 300 } 301 302 // calculateColumnSizes calculate size for each column 303 func calculateColumnSizes(t *Table) { 304 totalColumns := getColumnsNum(t) 305 t.columnSizes = make([]int, totalColumns) 306 307 if len(t.Sizes) != 0 { 308 for columnIndex := range t.Sizes { 309 if columnIndex < totalColumns { 310 t.columnSizes[columnIndex] = t.Sizes[columnIndex] 311 } 312 } 313 } 314 315 if len(t.data) > 0 { 316 for _, row := range t.data { 317 for index, item := range row { 318 itemSizes := strutil.Len(fmtc.Clean(item)) 319 320 if itemSizes > t.columnSizes[index] { 321 t.columnSizes[index] = itemSizes 322 } 323 } 324 } 325 } 326 327 if len(t.Headers) > 0 { 328 for index, header := range t.Headers { 329 headerSize := strutil.Len(header) 330 331 if headerSize > t.columnSizes[index] { 332 t.columnSizes[index] = headerSize 333 } 334 } 335 } 336 337 var fullSize int 338 339 windowWidth := getWindowWidth() 340 341 for columnIndex, columnSize := range t.columnSizes { 342 if columnIndex+1 == totalColumns { 343 t.columnSizes[columnIndex] = ((windowWidth - fullSize) - (totalColumns * 3)) + 1 344 } 345 346 fullSize += columnSize 347 } 348 } 349 350 // setColumnsSizes set columns sizes by number of columns 351 func setColumnsSizes(t *Table, columns int) { 352 windowWidth := getWindowWidth() 353 t.columnSizes = make([]int, columns) 354 355 totalSize := 0 356 columnSize := (windowWidth / columns) - 3 357 358 for index := range t.columnSizes { 359 t.columnSizes[index] = columnSize 360 totalSize += columnSize 361 362 if index+1 == columns { 363 if totalSize+(columns*3) < windowWidth { 364 t.columnSizes[index]++ 365 } 366 367 t.columnSizes[index]++ 368 } 369 } 370 } 371 372 // getColumnsNum return number of columns 373 func getColumnsNum(t *Table) int { 374 if len(t.data) > 0 { 375 var columns int 376 377 for _, row := range t.data { 378 rowColumns := len(row) 379 380 if rowColumns > columns { 381 columns = rowColumns 382 } 383 } 384 385 return columns 386 } 387 388 if len(t.Headers) > 0 { 389 return len(t.Headers) 390 } 391 392 return len(t.Sizes) 393 } 394 395 // formatText align text with color tags 396 func formatText(data string, size int, align uint8) string { 397 var dataSize int 398 399 if strings.Contains(data, "{") { 400 dataSize = strutil.Len(fmtc.Clean(data)) 401 } else { 402 dataSize = strutil.Len(data) 403 } 404 405 if dataSize >= size { 406 return data 407 } 408 409 switch align { 410 case ALIGN_RIGHT: 411 return strings.Repeat(" ", size-dataSize) + data 412 413 case ALIGN_CENTER: 414 prefixSize := (size - dataSize) / 2 415 suffixSize := size - (prefixSize + dataSize) 416 return strings.Repeat(" ", prefixSize) + data + strings.Repeat(" ", suffixSize) 417 } 418 419 return data + strings.Repeat(" ", size-dataSize) 420 } 421 422 // getAlignment return align for given column 423 func getAlignment(t *Table, columnIndex int) uint8 { 424 l := len(t.Alignment) 425 426 if l == 0 || columnIndex >= l { 427 return 0 428 } 429 430 return t.Alignment[columnIndex] 431 } 432 433 // getSeparatorSize return separator size based on size of all columns 434 func getSeparatorSize(t *Table) int { 435 if len(t.columnSizes) == 0 { 436 return getWindowWidth() 437 } 438 439 var size int 440 441 for _, columnSize := range t.columnSizes { 442 size += columnSize 443 } 444 445 return size + (len(t.columnSizes) * 3) - 1 446 } 447 448 // getWindowWidth return window width 449 func getWindowWidth() int { 450 if MaxWidth > 0 { 451 return mathutil.Between(MaxWidth, 80, 9999) 452 } 453 454 return mathutil.Between(window.GetWidth(), 80, 9999) 455 }