tabular.c (17565B)
1#define _XOPEN_SOURCE 2 3#include "tabular.h" 4#include "util.h" 5 6#include <sys/ioctl.h> 7#include <wchar.h> 8#include <errno.h> 9#include <unistd.h> 10#include <stdio.h> 11#include <string.h> 12#include <stdint.h> 13#include <stddef.h> 14 15#define BIT(i) (1U << (i)) 16 17struct col_state { 18 size_t width; 19 size_t written; 20 size_t maxlen; 21 size_t lpad, rpad; 22 bool hidden; 23}; 24 25struct fmt_state { 26 struct col_state *columns; 27 size_t column_count; 28 29 size_t hseplen; 30 size_t vseplen; 31 size_t xseplen; 32 size_t mseplen; 33 34 size_t line_limit; 35 size_t row_limit; 36 37 bool header_line; 38}; 39 40static void calc_word_aware(struct tabular_entry *entry, size_t maxwidth, 41 size_t *offset, size_t *width, size_t *lines); 42static size_t recalc_col_word_aware(const struct tabular_cfg *cfg, 43 struct tabular_row *rows, size_t limit, 44 size_t col, size_t maxwidth); 45static size_t calc_row_width(struct fmt_state *fmt); 46static size_t calc_output_lines(const struct tabular_cfg *cfg, 47 struct fmt_state *fmt, struct tabular_row *rows, size_t limit); 48static bool recalc_cols(const struct tabular_cfg *cfg, 49 struct tabular_stats *stats, struct fmt_state *fmt, 50 struct tabular_row *rows, size_t limit); 51 52static int calc_params_row(const struct tabular_cfg *cfg, 53 struct tabular_stats *stats, struct tabular_row *rows, 54 struct tabular_row *row, struct fmt_state *fmt, size_t limit); 55static int calc_params(const struct tabular_cfg *cfg, 56 struct tabular_stats *stat, struct tabular_row **rows, 57 struct fmt_state *fmt); 58 59static void output_header(FILE *file, const struct tabular_cfg *cfg, 60 struct tabular_stats *stat, const struct fmt_state *fmt); 61static void output_row(FILE *file, const struct tabular_cfg *cfg, 62 struct tabular_stats *stat, const struct tabular_row *row, 63 struct fmt_state *fmt); 64static int output_rows(FILE *file, const struct tabular_cfg *cfg, 65 struct tabular_stats *stats, struct tabular_row *rows, 66 struct fmt_state *fmt); 67 68const char * 69calc_word_aware_line(const char *str, size_t maxwidth, 70 size_t *out_offset, size_t *out_width) 71{ 72 const char *sep, *nstr; 73 size_t width, nwidth; 74 size_t offset, wlen; 75 76 if (!str) { 77 if (out_offset) *out_offset = 0; 78 if (out_width) *out_width = 0; 79 return NULL; 80 } 81 82 offset = strspn(str, " \v\t\r\n"); 83 str += offset; 84 85 nstr = str; 86 nwidth = width = 0; 87 while (nwidth <= maxwidth) { 88 width = nwidth; 89 str = nstr; 90 if (!str) break; 91 sep = strchr(str, ' '); 92 if (!sep) { 93 wlen = u8strlen(str); 94 nstr = NULL; 95 } else { 96 wlen = u8strnlen(str, (size_t) (sep - str)); 97 nstr = sep + 1; 98 } 99 nwidth = width + (width > 0) + wlen; 100 } 101 102 if (out_offset) *out_offset = offset; 103 if (out_width) { 104 /* single word > maxwidth */ 105 if (!width && nwidth) { 106 *out_width = maxwidth; 107 str = nstr; /* skip single word */ 108 } else { 109 *out_width = width; 110 } 111 } 112 113 return str; 114} 115 116void 117calc_word_aware(struct tabular_entry *entry, size_t maxwidth, 118 size_t *out_offset, size_t *out_width, size_t *out_lines) 119{ 120 const char *str; 121 size_t width, mwidth; 122 size_t lines; 123 124 if (out_offset) *out_offset = 0; 125 126 lines = 0; 127 mwidth = 0; 128 str = entry->str; 129 while (str) { 130 str = calc_word_aware_line(str, maxwidth, 131 str == entry->str ? out_offset : NULL, &width); 132 lines++; 133 mwidth = MAX(mwidth, width); 134 } 135 136 if (out_width) *out_width = mwidth; 137 if (out_lines) *out_lines = lines; 138} 139 140size_t 141recalc_col_word_aware(const struct tabular_cfg *cfg, 142 struct tabular_row *rows, size_t limit, size_t col, 143 size_t maxwidth) 144{ 145 size_t off, wwidth, max_wwidth, rowcnt; 146 struct tabular_row *row; 147 148 max_wwidth = cfg->columns[col].minwidth; 149 150 rowcnt = 0; 151 for (row = rows; row && rowcnt < limit; row = row->next) { 152 calc_word_aware(&row->entries[col], 153 maxwidth, &off, &wwidth, NULL); 154 max_wwidth = MAX(max_wwidth, wwidth); 155 rowcnt += 1; 156 } 157 158 return max_wwidth; 159} 160 161size_t 162calc_row_width(struct fmt_state *fmt) 163{ 164 size_t sum, i; 165 166 sum = 0; 167 for (i = 0; i < fmt->column_count; i++) { 168 if (fmt->columns[i].hidden) continue; 169 sum += fmt->columns[i].width + (sum ? fmt->hseplen : 0); 170 } 171 172 return sum; 173} 174 175size_t 176calc_output_lines(const struct tabular_cfg *cfg, struct fmt_state *fmt, 177 struct tabular_row *rows, size_t limit) 178{ 179 struct tabular_row *row; 180 size_t row_index, row_lines; 181 size_t entry_lines; 182 size_t lines, i; 183 size_t width; 184 185 lines = 0; 186 row_index = 0; 187 for (row = rows; row; row = row->next) { 188 if (row_index == limit) break; 189 190 row_lines = 0; 191 for (i = 0; i < cfg->column_count; i++) { 192 if (fmt->columns[i].hidden) continue; 193 width = fmt->columns[i].width 194 - fmt->columns[i].lpad - fmt->columns[i].rpad; 195 switch (cfg->columns[i].strategy) { 196 case TABULAR_TRUNC: 197 entry_lines = 1; 198 break; 199 case TABULAR_WRAP: 200 entry_lines = CEILDIV(row->entries[i].ulen, width); 201 break; 202 case TABULAR_WRAP_WORDAWARE: 203 calc_word_aware(&row->entries[i], 204 width, NULL, NULL, &entry_lines); 205 break; 206 } 207 row_lines = MAX(row_lines, entry_lines); 208 } 209 lines += row_lines; 210 211 row_index += 1; 212 } 213 214 return lines; 215} 216 217bool 218recalc_cols(const struct tabular_cfg *cfg, struct tabular_stats *stats, 219 struct fmt_state *fmt, struct tabular_row *rows, size_t limit) 220{ 221 size_t width, fullwidth, remaining; 222 ssize_t i; 223 224 fullwidth = cfg->outw - cfg->lpad - cfg->rpad; 225 226 /* reset widths to minimum requirement */ 227 for (i = 0; i < cfg->column_count; i++) { 228 if (fmt->columns[i].hidden) continue; 229 if (cfg->columns[i].squashable) { 230 fmt->columns[i].width = cfg->columns[i].minwidth; 231 } else { 232 width = MIN(fmt->columns[i].maxlen, 233 cfg->columns[i].maxwidth - cfg->columns[i].lpad 234 - cfg->columns[i].rpad); 235 if (cfg->columns[i].strategy == TABULAR_WRAP_WORDAWARE) 236 width = recalc_col_word_aware(cfg, rows, 237 limit, (size_t) i, width); 238 fmt->columns[i].width = width + cfg->columns[i].lpad 239 + cfg->columns[i].rpad; 240 } 241 } 242 243 /* could not fit all necessary columns at minimum width */ 244 width = calc_row_width(fmt); 245 while (width > fullwidth) { 246 /* remove non-essential columns */ 247 for (i = ((ssize_t) cfg->column_count) - 1; i >= 0; i--) { 248 if (!cfg->columns[i].essential 249 && !fmt->columns[i].hidden) { 250 fmt->columns[i].hidden = true; 251 break; 252 } 253 } 254 255 /* failed to remove any more */ 256 if (i < 0) return false; 257 258 stats->cols_truncated = true; 259 width = calc_row_width(fmt); 260 } 261 262 /* redistribute excess width to columns, left to right */ 263 remaining = fullwidth - width; 264 for (i = 0; remaining > 0 && i < cfg->column_count; i++) { 265 if (fmt->columns[i].hidden) continue; 266 if (!cfg->columns[i].squashable) continue; 267 width = MIN(fmt->columns[i].maxlen, cfg->columns[i].maxwidth 268 - fmt->columns[i].lpad - fmt->columns[i].rpad); 269 width = MIN(width, fmt->columns[i].width + remaining 270 - fmt->columns[i].lpad - fmt->columns[i].rpad); 271 if (cfg->columns[i].strategy == TABULAR_WRAP_WORDAWARE) 272 width = recalc_col_word_aware(cfg, rows, 273 limit, (size_t) i, width); 274 width = MAX(width + fmt->columns[i].lpad + fmt->columns[i].rpad, 275 fmt->columns[i].width); 276 remaining -= width - fmt->columns[i].width; 277 fmt->columns[i].width = width; 278 } 279 280 return true; 281} 282 283int 284calc_params_row(const struct tabular_cfg *cfg, struct tabular_stats *stats, 285 struct tabular_row *rows, struct tabular_row *row, 286 struct fmt_state *fmt, size_t limit) 287{ 288 struct colinfo *copy; 289 size_t i, height; 290 int rc; 291 292 /* make copy of current width in case the next row does not fit */ 293 copy = cfg->allocator->alloc(cfg->allocator, 294 sizeof(struct col_state) * cfg->column_count, &rc); 295 if (!copy) return -rc; 296 memcpy(copy, fmt->columns, sizeof(struct col_state) * cfg->column_count); 297 298 /* unhide cols */ 299 for (i = 0; i < cfg->column_count; i++) { 300 tabular_load_row_entry_hidden(cfg, row, i); 301 if (fmt->columns[i].hidden) { 302 fmt->columns[i].hidden = 303 (row->entries[i].flags & BIT(TABULAR_ENTRY_HIDDEN)); 304 } 305 if (fmt->columns[i].hidden) continue; 306 } 307 308 /* update maxlen for visible cols */ 309 for (i = 0; i < cfg->column_count; i++) { 310 tabular_load_row_entry_str(cfg, row, i); 311 fmt->columns[i].maxlen = MAX(fmt->columns[i].maxlen, 312 row->entries[i].ulen); 313 } 314 315 /* recalc column sizes to fit new column */ 316 if (!recalc_cols(cfg, stats, fmt, rows, limit)) 317 goto undo; 318 319 height = calc_output_lines(cfg, fmt, rows, limit); 320 321 /* check the line limit if applicable */ 322 if (fmt->line_limit > 0 && height > fmt->line_limit) 323 goto undo; 324 325 cfg->allocator->free(cfg->allocator, copy); 326 327 return 0; 328 329undo: 330 memcpy(fmt->columns, copy, sizeof(struct col_state) * fmt->column_count); 331 cfg->allocator->free(cfg->allocator, copy); 332 333 return 1; 334} 335 336int 337calc_params(const struct tabular_cfg *cfg, struct tabular_stats *stats, 338 struct tabular_row **rows, struct fmt_state *fmt) 339{ 340 struct tabular_row **row; 341 ssize_t first, last; 342 size_t i; 343 int rc; 344 345 fmt->header_line = ((cfg->vsep != NULL && *cfg->vsep) 346 && (cfg->xsep != NULL && *cfg->xsep)); 347 348 fmt->line_limit = 0; 349 if (cfg->fit_rows) { 350 fmt->line_limit = MAX(0, cfg->outh 351 - cfg->skip_lines - 1 - fmt->header_line); 352 } 353 354 fmt->hseplen = u8strlen(cfg->hsep); 355 fmt->vseplen = u8strlen(cfg->vsep); 356 fmt->xseplen = u8strlen(cfg->xsep); 357 fmt->mseplen = MAX(fmt->hseplen, fmt->xseplen); 358 359 fmt->column_count = cfg->column_count; 360 fmt->columns = cfg->allocator->alloc(cfg->allocator, 361 fmt->column_count * sizeof(struct col_state), &rc); 362 if (!fmt->columns) return -rc; 363 364 for (i = 0; i < cfg->column_count; i++) { 365 fmt->columns[i].lpad = cfg->columns[i].lpad; 366 fmt->columns[i].rpad = cfg->columns[i].rpad; 367 fmt->columns[i].hidden = true; 368 fmt->columns[i].width = cfg->columns[i].minwidth; 369 fmt->columns[i].maxlen = u8strlen(cfg->columns[i].name) 370 + fmt->columns[i].lpad + fmt->columns[i].rpad; 371 if (fmt->columns[i].maxlen > cfg->columns[i].minwidth) 372 return 1; 373 fmt->columns[i].written = 0; 374 } 375 376 fmt->row_limit = 0; 377 378 if (!*rows && cfg->row_gen) *rows = cfg->row_gen(&cfg->user); 379 if (!*rows) return 0; 380 381 if (!recalc_cols(cfg, stats, fmt, *rows, 0)) { 382 stats->cols_truncated = true; 383 stats->rows_truncated = true; 384 return 0; 385 } 386 387 for (row = rows; ; row = &(*row)->next) { 388 if (!*row && cfg->row_gen) *row = cfg->row_gen(&cfg->user); 389 if (!*row) break; 390 rc = calc_params_row(cfg, stats, *rows, 391 *row, fmt, fmt->row_limit + 1); 392 if (rc < 0) return rc; 393 if (rc > 0) { 394 stats->rows_truncated = true; 395 break; 396 } 397 fmt->row_limit++; 398 } 399 400 first = last = -1; 401 for (i = 0; i < cfg->column_count; i++) { 402 if (fmt->columns[i].hidden) continue; 403 if (first < 0) first = (ssize_t) i; 404 last = (ssize_t) i; 405 } 406 407 if (first >= 0 && last >= 0) { 408 fmt->columns[first].lpad += cfg->lpad; 409 fmt->columns[first].width += cfg->lpad; 410 fmt->columns[last].rpad += cfg->rpad; 411 fmt->columns[last].width += cfg->rpad; 412 } 413 414 return 0; 415} 416 417void 418output_header(FILE *file, const struct tabular_cfg *cfg, 419 struct tabular_stats *stats, const struct fmt_state *fmt) 420{ 421 size_t i, k, width; 422 bool dirty, first; 423 424 first = true; 425 for (i = 0; i < cfg->column_count; i++) { 426 if (fmt->columns[i].hidden) continue; 427 if (!first) { 428 dirty = cfg->print_style(file, cfg, NULL, NULL); 429 fprintf(file, "%s%*.s", cfg->hsep, 430 (int) (fmt->mseplen - fmt->hseplen), ""); 431 if (dirty) fprintf(file, "\x1b[0m"); 432 } 433 first = false; 434 435 width = fmt->columns[i].width 436 - fmt->columns[i].lpad - fmt->columns[i].rpad; 437 438 dirty = cfg->print_style(file, cfg, 439 NULL, &cfg->columns[i]); 440 print_pad(file, fmt->columns[i].lpad); 441 print_left(file, cfg->columns[i].name, width, width); 442 print_pad(file, fmt->columns[i].rpad); 443 if (dirty) fprintf(file, "\x1b[0m"); 444 } 445 446 fprintf(file, "\n"); 447 stats->lines_used += 1; 448 449 if (fmt->header_line) { 450 first = true; 451 dirty = cfg->print_style(file, cfg, NULL, NULL); 452 for (i = 0; i < cfg->column_count; i++) { 453 if (fmt->columns[i].hidden) continue; 454 if (!first) { 455 fprintf(file, "%s%*.s", cfg->xsep, 456 (int) (fmt->mseplen - fmt->xseplen), ""); 457 } 458 for (k = 0; k < fmt->columns[i].width; k++) 459 fprintf(file, "%s", cfg->vsep); 460 first = false; 461 } 462 if (dirty) fprintf(file, "\x1b[0m"); 463 fprintf(file, "\n"); 464 stats->lines_used += 1; 465 } 466} 467 468void 469output_row(FILE *file, const struct tabular_cfg *cfg, 470 struct tabular_stats *stat, const struct tabular_row *row, 471 struct fmt_state *fmt) 472{ 473 size_t wwidth, padwidth, outlen; 474 size_t i, off, width; 475 bool first, done, dirty; 476 char *entry; 477 478 for (i = 0; i < cfg->column_count; i++) { 479 fmt->columns[i].written = 0; 480 481 if (cfg->columns[i].strategy == TABULAR_TRUNC) { 482 width = fmt->columns[i].width 483 - fmt->columns[i].lpad - fmt->columns[i].rpad; 484 if (row->entries[i].ulen > width) { 485 width = u8rawlen(row->entries[i].str, width - 2); 486 row->entries[i].str[width] = '.'; 487 row->entries[i].str[width+1] = '.'; 488 row->entries[i].str[width+2] = '\0'; 489 } 490 } 491 } 492 493 do { 494 done = true; 495 first = true; 496 for (i = 0; i < cfg->column_count; i++) { 497 if (fmt->columns[i].hidden) continue; 498 499 if (!first) { 500 dirty = cfg->print_style(file, cfg, NULL, NULL); 501 fprintf(file, "%s", cfg->hsep); 502 if (dirty) fprintf(file, "\x1b[0m"); 503 } 504 first = false; 505 506 if (!row->entries[i].str) { 507 print_pad(file, fmt->columns[i].width); 508 continue; 509 } 510 511 entry = row->entries[i].str + fmt->columns[i].written; 512 513 dirty = cfg->print_style(file, cfg, row, &cfg->columns[i]); 514 515 off = 0; 516 width = fmt->columns[i].width 517 - fmt->columns[i].lpad - fmt->columns[i].rpad; 518 padwidth = width; 519 if (cfg->columns[i].strategy == TABULAR_WRAP_WORDAWARE) { 520 calc_word_aware_line(entry, width, &off, &wwidth); 521 entry += off; 522 width = wwidth; 523 } 524 525 print_pad(file, fmt->columns[i].lpad); 526 if (cfg->columns[i].align == TABULAR_ALIGN_LEFT) { 527 print_left(file, entry, width, padwidth); 528 } else if (cfg->columns[i].align == TABULAR_ALIGN_RIGHT) { 529 print_right(file, entry, width, padwidth); 530 } else { 531 print_center(file, entry, width, padwidth); 532 } 533 print_pad(file, fmt->columns[i].rpad); 534 535 if (dirty) fprintf(file, "\x1b[0m"); 536 537 outlen = MIN(u8strlen(entry), width + off); 538 fmt->columns[i].written += u8rawlen(entry, outlen); 539 540 /* check if anything other than whitespace left */ 541 entry += u8rawlen(entry, outlen); 542 if (strspn(entry, " \t\v\r\n") != u8strlen(entry)) 543 done = false; 544 } 545 fprintf(file, "\n"); 546 stat->lines_used += 1; 547 } while (!done); 548 549 stat->rows_displayed += 1; 550} 551 552int 553output_rows(FILE *file, const struct tabular_cfg *cfg, 554 struct tabular_stats *stats, struct tabular_row *rows, 555 struct fmt_state *fmt) 556{ 557 struct tabular_row *row; 558 size_t count; 559 560 if (fmt->row_limit == 0) return 0; 561 562 output_header(file, cfg, stats, fmt); 563 564 count = 0; 565 for (row = rows; row; row = row->next) { 566 if (count == fmt->row_limit) 567 break; 568 output_row(file, cfg, stats, row, fmt); 569 count += 1; 570 } 571 572 return 0; 573} 574 575int 576tabular_format(FILE *file, const struct tabular_cfg *cfg, 577 struct tabular_stats *stats, struct tabular_row **rows) 578{ 579 struct fmt_state fmt; 580 size_t i; 581 int rc; 582 583 stats->lines_used = 0; 584 stats->rows_displayed = 0; 585 stats->rows_truncated = false; 586 stats->cols_truncated = false; 587 588 if (!rows) return 1; 589 590 if (!cfg->column_count) return 0; 591 592 for (i = 0; i < cfg->column_count; i++) { 593 if (cfg->columns[i].minwidth > cfg->columns[i].maxwidth) 594 return 1; 595 if (cfg->columns[i].lpad + cfg->columns[i].rpad 596 >= cfg->columns[i].minwidth) 597 return 1; 598 if (cfg->columns[i].strategy != TABULAR_TRUNC 599 && cfg->columns[i].strategy != TABULAR_WRAP 600 && cfg->columns[i].strategy != TABULAR_WRAP_WORDAWARE) 601 return 1; 602 } 603 604 rc = calc_params(cfg, stats, rows, &fmt); 605 if (rc) return rc; 606 607 rc = output_rows(file, cfg, stats, *rows, &fmt); 608 if (rc) return rc; 609 610 rc = cfg->allocator->free(cfg->allocator, fmt.columns); 611 if (rc) return -rc; 612 613 return 0; 614} 615 616struct tabular_row * 617tabular_alloc_row(const struct tabular_cfg *cfg, 618 int *rc, struct tabular_user user) 619{ 620 struct tabular_row *row; 621 622 row = cfg->allocator->alloc(cfg->allocator, 623 sizeof(struct tabular_row), rc); 624 if (!row && rc) *rc = -*rc; 625 if (!row) return NULL; 626 627 row->next = NULL; 628 row->user = user; 629 row->entries = cfg->allocator->alloc(cfg->allocator, 630 sizeof(struct tabular_entry) * cfg->column_count, rc); 631 if (!row->entries && rc) *rc = -*rc; 632 if (!row->entries) return NULL; 633 memset(row->entries, 0, sizeof(struct tabular_entry) * cfg->column_count); 634 635 return row; 636} 637 638int 639tabular_free_rows(const struct tabular_cfg *cfg, struct tabular_row *rows) 640{ 641 struct tabular_row *iter, *next; 642 size_t i; 643 int rc; 644 645 for (iter = rows; iter; iter = next) { 646 next = iter->next; 647 for (i = 0; i < cfg->column_count; i++) { 648 rc = cfg->allocator->free(cfg->allocator, 649 iter->entries[i].str); 650 if (rc) return -rc; 651 } 652 rc = cfg->allocator->free(cfg->allocator, iter->entries); 653 if (rc) return -rc; 654 rc = cfg->allocator->free(cfg->allocator, iter); 655 if (rc) return -rc; 656 } 657 658 return 0; 659} 660 661void 662tabular_load_row_entry_hidden(const struct tabular_cfg *cfg, 663 struct tabular_row *row, size_t col) 664{ 665 bool hidden; 666 667 if (row->entries[col].flags & BIT(TABULAR_ENTRY_HIDDEN_SET)) return; 668 669 if (cfg->columns[col].is_hidden) { 670 hidden = cfg->columns[col].is_hidden( 671 &row->user, &cfg->columns[col].user); 672 } else { 673 hidden = false; 674 } 675 676 if (hidden) { 677 row->entries[col].flags |= BIT(TABULAR_ENTRY_HIDDEN); 678 } else { 679 row->entries[col].flags &= ~BIT(TABULAR_ENTRY_HIDDEN); 680 } 681 682 row->entries[col].flags |= BIT(TABULAR_ENTRY_HIDDEN_SET); 683} 684 685void 686tabular_load_row_entry_str(const struct tabular_cfg *cfg, 687 struct tabular_row *row, size_t col) 688{ 689 if (row->entries[col].flags & BIT(TABULAR_ENTRY_STR_SET)) return; 690 691 row->entries[col].str = cfg->columns[col].to_str( 692 &row->user, &cfg->columns[col].user); 693 row->entries[col].ulen = (uint32_t) u8strlen(row->entries[col].str); 694 row->entries[col].flags |= BIT(TABULAR_ENTRY_STR_SET); 695}