2 * TT - Table or Tree, features:
3 * - column width could be defined as absolute or relative to the terminal width
4 * - allows to truncate or wrap data in columns
5 * - prints tree if parent->child relation is defined
6 * - draws the tree by ASCII or UTF8 lines (depends on terminal setting)
8 * Copyright (C) 2010 Karel Zak <kzak@redhat.com>
10 * This file may be redistributed under the terms of the
11 * GNU Lesser General Public License.
18 #ifdef HAVE_SYS_IOCTL_H
19 #include <sys/ioctl.h>
33 static const struct tt_symbols ascii_tt_symbols
= {
40 #define mbs_width(_s) mbstowcs(NULL, _s, 0)
42 #define UTF_V "\342\224\202" /* U+2502, Vertical line drawing char */
43 #define UTF_VR "\342\224\234" /* U+251C, Vertical and right */
44 #define UTF_H "\342\224\200" /* U+2500, Horizontal */
45 #define UTF_UR "\342\224\224" /* U+2514, Up and right */
47 static const struct tt_symbols utf8_tt_symbols
= {
48 .branch
= UTF_VR UTF_H
,
50 .right
= UTF_UR UTF_H
,
53 #else /* !HAVE_WIDECHAR */
54 # define mbs_width strlen(_s)
55 #endif /* !HAVE_WIDECHAR */
57 #define is_last_column(_tb, _cl) \
58 list_last_entry(&(_cl)->cl_columns, &(_tb)->tb_columns)
60 /* TODO: move to lib/mbalign.c */
62 static size_t wc_truncate (wchar_t *wc
, size_t width
)
69 next_cells
= wcwidth (*wc
);
70 if (next_cells
== -1) /* non printable */
72 *wc
= 0xFFFD; /* L'\uFFFD' (replacement char) */
75 if (cells
+ next_cells
> width
)
86 /* TODO: move to lib/mbalign.c */
87 static size_t mbs_truncate(char *str
, size_t width
)
89 size_t bytes
= strlen(str
) + 1;
91 size_t sz
= mbs_width(str
);
96 return sz
; /* truncate is unnecessary */
98 if (sz
== (size_t) -1)
101 wcs
= malloc(sz
* sizeof(wchar_t));
105 if (!mbstowcs(wcs
, str
, sz
))
107 rc
= wc_truncate(wcs
, width
);
108 wcstombs(str
, wcs
, bytes
);
117 return bytes
; /* truncate is unnecessary */
122 * @flags: TT_FL_* flags (usually TT_FL_{ASCII,RAW})
124 * Returns: newly allocated table
126 struct tt
*tt_new_table(int flags
)
130 tb
= calloc(1, sizeof(struct tt
));
135 INIT_LIST_HEAD(&tb
->tb_lines
);
136 INIT_LIST_HEAD(&tb
->tb_columns
);
138 #if defined(HAVE_WIDECHAR)
139 if (!(flags
& TT_FL_ASCII
) && !strcmp(nl_langinfo(CODESET
), "UTF-8"))
140 tb
->symbols
= &utf8_tt_symbols
;
143 tb
->symbols
= &ascii_tt_symbols
;
147 void tt_free_table(struct tt
*tb
)
151 while (!list_empty(&tb
->tb_lines
)) {
152 struct tt_line
*ln
= list_entry(tb
->tb_lines
.next
,
153 struct tt_line
, ln_lines
);
154 list_del(&ln
->ln_lines
);
158 while (!list_empty(&tb
->tb_columns
)) {
159 struct tt_column
*cl
= list_entry(tb
->tb_columns
.next
,
160 struct tt_column
, cl_columns
);
161 list_del(&cl
->cl_columns
);
169 * @name: column header
170 * @whint: column width hint (absolute width: N > 1; relative width: N < 1)
171 * @flags: usually TT_FL_{TREE,TRUNCATE}
173 * The column is necessary to address (for example for tt_line_set_data()) by
174 * sequential number. The first defined column has the colnum = 0. For example:
176 * tt_define_column(tab, "FOO", 0.5, 0); // colnum = 0
177 * tt_define_column(tab, "BAR", 0.5, 0); // colnum = 1
180 * tt_line_set_data(line, 0, "foo-data"); // FOO column
181 * tt_line_set_data(line, 1, "bar-data"); // BAR column
183 * Returns: newly allocated column definition
185 struct tt_column
*tt_define_column(struct tt
*tb
, const char *name
,
186 double whint
, int flags
)
188 struct tt_column
*cl
;
192 cl
= calloc(1, sizeof(*cl
));
197 cl
->width_hint
= whint
;
199 cl
->seqnum
= tb
->ncols
++;
201 if (flags
& TT_FL_TREE
)
202 tb
->flags
|= TT_FL_TREE
;
204 INIT_LIST_HEAD(&cl
->cl_columns
);
205 list_add_tail(&cl
->cl_columns
, &tb
->tb_columns
);
211 * @parent: parental line or NULL
213 * Returns: newly allocate line
215 struct tt_line
*tt_add_line(struct tt
*tb
, struct tt_line
*parent
)
217 struct tt_line
*ln
= NULL
;
219 if (!tb
|| !tb
->ncols
)
221 ln
= calloc(1, sizeof(*ln
));
224 ln
->data
= calloc(tb
->ncols
, sizeof(char *));
230 INIT_LIST_HEAD(&ln
->ln_lines
);
231 INIT_LIST_HEAD(&ln
->ln_children
);
232 INIT_LIST_HEAD(&ln
->ln_branch
);
234 list_add_tail(&ln
->ln_lines
, &tb
->tb_lines
);
237 list_add_tail(&ln
->ln_children
, &parent
->ln_branch
);
246 * @colnum: number of column (0..N)
248 * Returns: pointer to column or NULL
250 struct tt_column
*tt_get_column(struct tt
*tb
, int colnum
)
254 list_for_each(p
, &tb
->tb_columns
) {
255 struct tt_column
*cl
=
256 list_entry(p
, struct tt_column
, cl_columns
);
257 if (cl
->seqnum
== colnum
)
265 * @colnum: number of column (0..N)
266 * @data: printable data
268 * Stores data that will be printed to the table cell.
270 int tt_line_set_data(struct tt_line
*ln
, int colnum
, const char *data
)
272 struct tt_column
*cl
;
276 cl
= tt_get_column(ln
->table
, colnum
);
280 if (ln
->data
[cl
->seqnum
])
281 ln
->data_sz
-= strlen(ln
->data
[cl
->seqnum
]);
283 ln
->data
[cl
->seqnum
] = data
;
285 ln
->data_sz
+= strlen(data
);
289 static int get_terminal_width(void)
292 struct ttysize t_win
;
295 struct winsize w_win
;
300 if (ioctl (0, TIOCGSIZE
, &t_win
) == 0)
301 return t_win
.ts_cols
;
304 if (ioctl (0, TIOCGWINSZ
, &w_win
) == 0)
307 cp
= getenv("COLUMNS");
309 return strtol(cp
, NULL
, 10);
313 int tt_line_set_userdata(struct tt_line
*ln
, void *data
)
321 static char *line_get_ascii_art(struct tt_line
*ln
, char *buf
, size_t *bufsz
)
329 buf
= line_get_ascii_art(ln
->parent
, buf
, bufsz
);
333 if (list_last_entry(&ln
->ln_children
, &ln
->parent
->ln_branch
))
336 art
= ln
->table
->symbols
->vert
;
340 return NULL
; /* no space, internal error */
342 memcpy(buf
, art
, len
);
347 static char *line_get_data(struct tt_line
*ln
, struct tt_column
*cl
,
348 char *buf
, size_t bufsz
)
350 const char *data
= ln
->data
[cl
->seqnum
];
351 const struct tt_symbols
*sym
;
354 memset(buf
, 0, bufsz
);
358 if (!(cl
->flags
& TT_FL_TREE
)) {
359 strncpy(buf
, data
, bufsz
);
360 buf
[bufsz
- 1] = '\0';
364 p
= line_get_ascii_art(ln
->parent
, buf
, &bufsz
);
369 sym
= ln
->table
->symbols
;
372 snprintf(p
, bufsz
, "%s", data
); /* root node */
373 else if (list_last_entry(&ln
->ln_children
, &ln
->parent
->ln_branch
))
374 snprintf(p
, bufsz
, "%s%s", sym
->right
, data
); /* last chaild */
376 snprintf(p
, bufsz
, "%s%s", sym
->branch
, data
); /* any child */
381 static void recount_widths(struct tt
*tb
, char *buf
, size_t bufsz
)
384 int width
= 0, trunc_only
;
386 /* set width according to the size of data
388 list_for_each(p
, &tb
->tb_columns
) {
389 struct tt_column
*cl
=
390 list_entry(p
, struct tt_column
, cl_columns
);
391 struct list_head
*lp
;
393 list_for_each(lp
, &tb
->tb_lines
) {
395 list_entry(lp
, struct tt_line
, ln_lines
);
397 char *data
= line_get_data(ln
, cl
, buf
, bufsz
);
398 size_t len
= data
? mbs_width(data
) : 0;
405 /* set minimal width (= size of column header)
407 list_for_each(p
, &tb
->tb_columns
) {
408 struct tt_column
*cl
=
409 list_entry(p
, struct tt_column
, cl_columns
);
412 cl
->width_min
= mbs_width(cl
->name
);
414 if (cl
->width
< cl
->width_min
)
415 cl
->width
= cl
->width_min
;
417 else if (cl
->width_hint
>= 1 &&
418 cl
->width
< (int) cl
->width_hint
&&
419 cl
->width_min
< (int) cl
->width_hint
)
421 cl
->width
= (int) cl
->width_hint
;
423 width
+= cl
->width
+ (is_last_column(tb
, cl
) ? 0 : 1);
426 if (width
== tb
->termwidth
)
428 if (width
< tb
->termwidth
) {
429 /* cool, use the extra space for the last column */
430 struct tt_column
*cl
= list_entry(
431 tb
->tb_columns
.prev
, struct tt_column
, cl_columns
);
433 if (!(cl
->flags
& TT_FL_RIGHT
))
434 cl
->width
+= tb
->termwidth
- width
;
438 /* bad, we have to reduce output width, this is done in two steps:
439 * 1/ reduce columns with a relative width and with truncate flag
440 * 2) reduce columns with a relative width without truncate flag
443 while(width
> tb
->termwidth
) {
446 list_for_each(p
, &tb
->tb_columns
) {
447 struct tt_column
*cl
=
448 list_entry(p
, struct tt_column
, cl_columns
);
450 if (width
<= tb
->termwidth
)
452 if (cl
->width_hint
> 1)
453 continue; /* never truncate columns with absolute sizes */
454 if (cl
->flags
& TT_FL_TREE
)
455 continue; /* never truncate the tree */
456 if (trunc_only
&& !(cl
->flags
& TT_FL_TRUNC
))
458 if (cl
->width
== cl
->width_min
)
460 if (cl
->width
> cl
->width_hint
* tb
->termwidth
) {
474 fprintf(stderr, "terminal: %d, output: %d\n", tb->termwidth, width);
476 list_for_each(p, &tb->tb_columns) {
477 struct tt_column *cl =
478 list_entry(p, struct tt_column, cl_columns);
480 fprintf(stderr, "width: %s=%d [hint=%d]\n",
482 cl->width_hint > 1 ? (int) cl->width_hint :
483 (int) (cl->width_hint * tb->termwidth));
489 /* note that this function modifies @data
491 static void print_data(struct tt
*tb
, struct tt_column
*cl
, char *data
)
500 if (tb
->flags
& TT_FL_RAW
) {
502 if (!is_last_column(tb
, cl
))
507 /* note that 'len' and 'width' are number of cells, not bytes */
508 len
= mbs_width(data
);
510 if (!len
|| len
== (size_t) -1) {
516 if (is_last_column(tb
, cl
) && len
< width
)
520 if (len
> width
&& (cl
->flags
& TT_FL_TRUNC
)) {
521 len
= mbs_truncate(data
, width
);
522 if (!data
|| len
== (size_t) -1) {
528 if (!(tb
->flags
& TT_FL_RAW
) && (cl
->flags
& TT_FL_RIGHT
)) {
530 fprintf(stdout
, "%*s", xw
, data
);
537 for (i
= len
; i
< width
; i
++)
538 fputc(' ', stdout
); /* padding */
540 if (!is_last_column(tb
, cl
)) {
541 if (len
> width
&& !(cl
->flags
& TT_FL_TRUNC
)) {
543 for (i
= 0; i
<= cl
->seqnum
; i
++) {
544 struct tt_column
*x
= tt_get_column(tb
, i
);
545 printf("%*s ", -x
->width
, " ");
548 fputc(' ', stdout
); /* columns separator */
552 static void print_line(struct tt_line
*ln
, char *buf
, size_t bufsz
)
556 /* set width according to the size of data
558 list_for_each(p
, &ln
->table
->tb_columns
) {
559 struct tt_column
*cl
=
560 list_entry(p
, struct tt_column
, cl_columns
);
562 print_data(ln
->table
, cl
, line_get_data(ln
, cl
, buf
, bufsz
));
567 static void print_header(struct tt
*tb
, char *buf
, size_t bufsz
)
571 if ((tb
->flags
& TT_FL_NOHEADINGS
) || list_empty(&tb
->tb_lines
))
574 /* set width according to the size of data
576 list_for_each(p
, &tb
->tb_columns
) {
577 struct tt_column
*cl
=
578 list_entry(p
, struct tt_column
, cl_columns
);
580 strncpy(buf
, cl
->name
, bufsz
);
581 buf
[bufsz
- 1] = '\0';
582 print_data(tb
, cl
, buf
);
587 static void print_table(struct tt
*tb
, char *buf
, size_t bufsz
)
591 print_header(tb
, buf
, bufsz
);
593 list_for_each(p
, &tb
->tb_lines
) {
594 struct tt_line
*ln
= list_entry(p
, struct tt_line
, ln_lines
);
596 print_line(ln
, buf
, bufsz
);
600 static void print_tree_line(struct tt_line
*ln
, char *buf
, size_t bufsz
)
604 print_line(ln
, buf
, bufsz
);
606 if (list_empty(&ln
->ln_branch
))
609 /* print all children */
610 list_for_each(p
, &ln
->ln_branch
) {
611 struct tt_line
*chld
=
612 list_entry(p
, struct tt_line
, ln_children
);
613 print_tree_line(chld
, buf
, bufsz
);
617 static void print_tree(struct tt
*tb
, char *buf
, size_t bufsz
)
621 print_header(tb
, buf
, bufsz
);
623 list_for_each(p
, &tb
->tb_lines
) {
624 struct tt_line
*ln
= list_entry(p
, struct tt_line
, ln_lines
);
629 print_tree_line(ln
, buf
, bufsz
);
636 * Prints the table to stdout
638 int tt_print_table(struct tt
*tb
)
646 if (!tb
->termwidth
) {
647 tb
->termwidth
= get_terminal_width();
648 if (tb
->termwidth
<= 0)
652 line_sz
= tb
->termwidth
;
654 list_for_each(p
, &tb
->tb_lines
) {
655 struct tt_line
*ln
= list_entry(p
, struct tt_line
, ln_lines
);
656 if (ln
->data_sz
> line_sz
)
657 line_sz
= ln
->data_sz
;
660 line
= malloc(line_sz
);
663 if (!(tb
->flags
& TT_FL_RAW
))
664 recount_widths(tb
, line
, line_sz
);
665 if (tb
->flags
& TT_FL_TREE
)
666 print_tree(tb
, line
, line_sz
);
668 print_table(tb
, line
, line_sz
);
674 int tt_parse_columns_list(const char *list
, int cols
[], int *ncols
,
675 int (name2id
)(const char *, size_t))
677 const char *begin
= NULL
, *p
;
679 if (!list
|| !*list
|| !cols
|| !ncols
|| !name2id
)
684 for (p
= list
; p
&& *p
; p
++) {
685 const char *end
= NULL
;
689 begin
= p
; /* begin of the column name */
691 end
= p
; /* terminate the name */
692 if (*(p
+ 1) == '\0')
693 end
= p
+ 1; /* end of string */
699 id
= name2id(begin
, end
- begin
);
714 enum { MYCOL_NAME
, MYCOL_FOO
, MYCOL_BAR
, MYCOL_PATH
};
716 int main(int argc
, char *argv
[])
719 struct tt_line
*ln
, *pr
, *root
;
720 int flags
= 0, notree
= 0, i
;
722 if (argc
== 2 && !strcmp(argv
[1], "--help")) {
723 printf("%s [--ascii | --raw | --list]\n",
724 program_invocation_short_name
);
726 } else if (argc
== 2 && !strcmp(argv
[1], "--ascii"))
727 flags
|= TT_FL_ASCII
;
728 else if (argc
== 2 && !strcmp(argv
[1], "--raw")) {
731 } else if (argc
== 2 && !strcmp(argv
[1], "--list"))
734 setlocale(LC_ALL
, "");
735 bindtextdomain(PACKAGE
, LOCALEDIR
);
738 tb
= tt_new_table(flags
);
740 err(EXIT_FAILURE
, "table initialization failed");
742 tt_define_column(tb
, "NAME", 0.3, notree
? 0 : TT_FL_TREE
);
743 tt_define_column(tb
, "FOO", 0.3, TT_FL_TRUNC
);
744 tt_define_column(tb
, "BAR", 0.3, 0);
745 tt_define_column(tb
, "PATH", 0.3, 0);
747 for (i
= 0; i
< 2; i
++) {
748 root
= ln
= tt_add_line(tb
, NULL
);
749 tt_line_set_data(ln
, MYCOL_NAME
, "AAA");
750 tt_line_set_data(ln
, MYCOL_FOO
, "a-foo-foo");
751 tt_line_set_data(ln
, MYCOL_BAR
, "barBar-A");
752 tt_line_set_data(ln
, MYCOL_PATH
, "/mnt/AAA");
754 pr
= ln
= tt_add_line(tb
, ln
);
755 tt_line_set_data(ln
, MYCOL_NAME
, "AAA.A");
756 tt_line_set_data(ln
, MYCOL_FOO
, "a.a-foo-foo");
757 tt_line_set_data(ln
, MYCOL_BAR
, "barBar-A.A");
758 tt_line_set_data(ln
, MYCOL_PATH
, "/mnt/AAA/A");
760 ln
= tt_add_line(tb
, pr
);
761 tt_line_set_data(ln
, MYCOL_NAME
, "AAA.A.AAA");
762 tt_line_set_data(ln
, MYCOL_FOO
, "a.a.a-foo-foo");
763 tt_line_set_data(ln
, MYCOL_BAR
, "barBar-A.A.A");
764 tt_line_set_data(ln
, MYCOL_PATH
, "/mnt/AAA/A/AAA");
766 ln
= tt_add_line(tb
, root
);
767 tt_line_set_data(ln
, MYCOL_NAME
, "AAA.B");
768 tt_line_set_data(ln
, MYCOL_FOO
, "a.b-foo-foo");
769 tt_line_set_data(ln
, MYCOL_BAR
, "barBar-A.B");
770 tt_line_set_data(ln
, MYCOL_PATH
, "/mnt/AAA/B");
772 ln
= tt_add_line(tb
, pr
);
773 tt_line_set_data(ln
, MYCOL_NAME
, "AAA.A.BBB");
774 tt_line_set_data(ln
, MYCOL_FOO
, "a.a.b-foo-foo");
775 tt_line_set_data(ln
, MYCOL_BAR
, "barBar-A.A.BBB");
776 tt_line_set_data(ln
, MYCOL_PATH
, "/mnt/AAA/A/BBB");
778 ln
= tt_add_line(tb
, pr
);
779 tt_line_set_data(ln
, MYCOL_NAME
, "AAA.A.CCC");
780 tt_line_set_data(ln
, MYCOL_FOO
, "a.a.c-foo-foo");
781 tt_line_set_data(ln
, MYCOL_BAR
, "barBar-A.A.CCC");
782 tt_line_set_data(ln
, MYCOL_PATH
, "/mnt/AAA/A/CCC");
784 ln
= tt_add_line(tb
, root
);
785 tt_line_set_data(ln
, MYCOL_NAME
, "AAA.C");
786 tt_line_set_data(ln
, MYCOL_FOO
, "a.c-foo-foo");
787 tt_line_set_data(ln
, MYCOL_BAR
, "barBar-A.C");
788 tt_line_set_data(ln
, MYCOL_PATH
, "/mnt/AAA/C");