diff --git a/include/table-printer.h b/include/table-printer.h new file mode 100644 index 0000000..592dff8 --- /dev/null +++ b/include/table-printer.h @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 Jim Ramsay + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE + * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +struct table_printer{ + FILE* stream; + int max_width; + int left_indent; + int left_width; + int column_offset; +}; + +// Sets default values for every subsequent table_printer_new (Optional: defaults to 80/4/8) +void table_printer_set_defaults(int max_width, int left_indent, + int column_offset); + +void table_printer_init(struct table_printer* self, FILE* stream, + int left_width); + +void table_printer_print_line(struct table_printer* self, const char* left_text, + const char* right_text); + +void table_printer_print_fmtline(struct table_printer* self, + const char* right_text, + const char* left_format, ...); + +int table_printer_reflow_text(char* dst, int dst_size, const char* src, + int width); + +void table_printer_indent_and_reflow_text(FILE* stream, const char* src, + int width, int first_line_indent, int subsequent_indent); + diff --git a/meson.build b/meson.build index 3b11823..34cbfb8 100644 --- a/meson.build +++ b/meson.build @@ -99,6 +99,7 @@ sources = [ 'src/ctl-server.c', 'src/ctl-commands.c', 'src/option-parser.c', + 'src/table-printer.c', ] dependencies = [ @@ -123,6 +124,7 @@ ctlsources = [ 'src/ctl-commands.c', 'src/strlcpy.c', 'src/option-parser.c', + 'src/table-printer.c', ] ctldependencies = [ diff --git a/src/option-parser.c b/src/option-parser.c index 1094ed0..e65f87e 100644 --- a/src/option-parser.c +++ b/src/option-parser.c @@ -16,6 +16,7 @@ #include "option-parser.h" #include "strlcpy.h" +#include "table-printer.h" #include #include @@ -72,82 +73,35 @@ static int get_left_col_width(const struct wv_option* opts, int n) return max_width; } -static void reflow_text(char* dst, const char* src, int width) -{ - int line_len = 0; - int last_space_pos = 0; - - int dst_len = 0; - int i = 0; - - while (src[i]) { - char c = src[i]; - - if (line_len > width) { - assert(last_space_pos > 0); - - dst_len -= i - last_space_pos; - dst[dst_len++] = '\n'; - i = last_space_pos + 1; - line_len = 0; - continue; - } - - if (c == ' ') - last_space_pos = i; - - dst[dst_len++] = c; - ++i; - ++line_len; - } - - dst[dst_len] = '\0'; -} - -static void format_option(const struct wv_option* opt, int left_col_width, - FILE* stream) +static void format_option(struct table_printer* printer, const struct wv_option* opt) { if (!opt->help) return; - int n_chars = fprintf(stream, " "); + int n_chars = 0; + char buf[64]; if (opt->short_opt) - n_chars += fprintf(stream, "-%c", opt->short_opt); + n_chars += snprintf(buf + n_chars, sizeof(buf) - n_chars, + "-%c", opt->short_opt); if (opt->long_opt) - n_chars += fprintf(stream, "%s--%s", - opt->short_opt ? "," : "", opt->long_opt); + n_chars += snprintf(buf + n_chars, sizeof(buf) - n_chars, + "%s--%s", opt->short_opt ? "," : "", + opt->long_opt); if (opt->schema) - n_chars += fprintf(stream, "%s%s", - opt->long_opt ? "=" : "", opt->schema); + n_chars += snprintf(buf + n_chars, sizeof(buf) - n_chars, + "%s%s", opt->long_opt ? "=" : "", opt->schema); - n_chars += fprintf(stream, "%*s", left_col_width - n_chars + 8, ""); - - int right_col_width = 80 - 8 - left_col_width; - assert(right_col_width >= 0); - - char help[256]; - reflow_text(help, opt->help, right_col_width); - - char* line = strtok(help, "\n"); - fprintf(stream, "%s\n", line); - - while (true) { - line = strtok(NULL, "\n"); - if (!line) - break; - - fprintf(stream, "%*s%s\n", left_col_width + 8, "", line); - } + table_printer_print_line(printer, buf, opt->help); } void option_parser_print_options(struct option_parser* self, FILE* stream) { fprintf(stream, "%s:\n", self->name); int left_col_width = get_left_col_width(self->options, self->n_opts); - - for (int i = 0; i < self->n_opts; ++i) { - format_option(&self->options[i], left_col_width, stream); - } + struct table_printer printer; + table_printer_init(&printer, stream, left_col_width); + for (int i = 0; i < self->n_opts; ++i) + format_option(&printer, &self->options[i]); } static const struct wv_option* find_long_option( diff --git a/src/table-printer.c b/src/table-printer.c new file mode 100644 index 0000000..1dcedce --- /dev/null +++ b/src/table-printer.c @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2023 Andri Yngvason + * Copyright (c) 2023 Jim Ramsay + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE + * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +#include "table-printer.h" + +#include +#include +#include +#include +#include + +static struct table_printer defaults = { + .max_width = 80, + .left_indent = 4, + .column_offset = 8, + .stream = NULL, + .left_width = 0, +}; + +void table_printer_set_defaults(int max_width, int left_indent, + int column_offset) +{ + defaults.max_width = max_width; + defaults.left_indent = left_indent; + defaults.column_offset = column_offset; +} + +void table_printer_init(struct table_printer* self, FILE* stream, + int left_width) +{ + memcpy(self, &defaults, sizeof(*self)); + self->stream = stream; + self->left_width = left_width; +} + +int table_printer_reflow_text(char* dst, int dst_size, const char* src, + int width) +{ + int line_len = 0; + int last_space_pos = 0; + + int dst_len = 0; + int i = 0; + + while (true) { + char c = src[i]; + if (line_len > width) { + // first word > width + assert(last_space_pos > 0); + // subsequent word > width + assert(dst[last_space_pos] != '\n'); + + dst_len -= i - last_space_pos; + dst[dst_len++] = '\n'; + i = last_space_pos + 1; + line_len = 0; + continue; + } + if (!c) + break; + + if (c == ' ') + last_space_pos = i; + dst[dst_len++] = c; + assert(dst_len < dst_size); + ++line_len; + ++i; + } + + dst[dst_len] = '\0'; + return dst_len; +} + +void table_printer_indent_and_reflow_text(FILE* stream, const char* src, + int width, int first_line_indent, int subsequent_indent) +{ + char buffer[256]; + table_printer_reflow_text(buffer, sizeof(buffer), src, width); + + char* line = strtok(buffer, "\n"); + fprintf(stream, "%*s%s\n", first_line_indent, "", line); + + while (true) { + line = strtok(NULL, "\n"); + if (!line) + break; + + fprintf(stream, "%*s%s\n", subsequent_indent, "", line); + } +} + +void table_printer_print_line(struct table_printer* self, const char* left_text, + const char* right_text) +{ + fprintf(self->stream, "%*s", self->left_indent, ""); + int field_len = fprintf(self->stream, "%s", left_text); + fprintf(self->stream, "%*s", self->left_width - field_len + self->column_offset, ""); + int column_indent = self->left_indent + self->left_width + self->column_offset; + int column_width = self->max_width - column_indent; + table_printer_indent_and_reflow_text(self->stream, + right_text, + column_width, 0, column_indent); +} + +void table_printer_print_fmtline(struct table_printer* self, + const char* right_text, + const char* left_format, ...) +{ + char buf[64]; + va_list args; + va_start(args, left_format); + vsnprintf(buf, sizeof(buf), left_format, args); + va_end(args); + table_printer_print_line(self, buf, right_text); +} + diff --git a/test/meson.build b/test/meson.build index e165990..73855c0 100644 --- a/test/meson.build +++ b/test/meson.build @@ -1,10 +1,18 @@ -option_parser = executable('option-parser', +test('table-printer', executable('table-printer', + [ + 'table-printer-test.c', + '../src/table-printer.c', + ], + include_directories: inc, + dependencies: [ ], +)) +test('option-parser', executable('option-parser', [ 'option-parser-test.c', '../src/option-parser.c', + '../src/table-printer.c', '../src/strlcpy.c', ], include_directories: inc, dependencies: [ ], -) -test('option-parser', option_parser) +)) diff --git a/test/table-printer-test.c b/test/table-printer-test.c new file mode 100644 index 0000000..ce51a7c --- /dev/null +++ b/test/table-printer-test.c @@ -0,0 +1,145 @@ +#include "tst.h" +#include "table-printer.h" +#include + +static int test_reflow_text(void) +{ + char buf[20]; + const char* src = "one two three four"; + int len; + + len = table_printer_reflow_text(buf, sizeof(buf), src, 20); + ASSERT_INT_EQ(18, len); + ASSERT_STR_EQ("one two three four", buf); + + len = table_printer_reflow_text(buf, sizeof(buf), src, 18); + ASSERT_INT_EQ(18, len); + ASSERT_STR_EQ("one two three four", buf); + + len = table_printer_reflow_text(buf, sizeof(buf), src, 17); + ASSERT_INT_EQ(18, len); + ASSERT_STR_EQ("one two three\nfour", buf); + + len = table_printer_reflow_text(buf, sizeof(buf), src, 10); + ASSERT_INT_EQ(18, len); + ASSERT_STR_EQ("one two\nthree four", buf); + + len = table_printer_reflow_text(buf, sizeof(buf), src, 8); + ASSERT_INT_EQ(18, len); + ASSERT_STR_EQ("one two\nthree\nfour", buf); + + len = table_printer_reflow_text(buf, sizeof(buf), src, 7); + ASSERT_INT_EQ(18, len); + ASSERT_STR_EQ("one two\nthree\nfour", buf); + + len = table_printer_reflow_text(buf, sizeof(buf), src, 6); + ASSERT_INT_EQ(18, len); + ASSERT_STR_EQ("one\ntwo\nthree\nfour", buf); + + len = table_printer_reflow_text(buf, sizeof(buf), src, 5); + ASSERT_INT_EQ(18, len); + ASSERT_STR_EQ("one\ntwo\nthree\nfour", buf); + + // width <= 4 cause aborts (if any word length > width) + + return 0; +} + +static int test_indent_and_reflow(void) +{ + size_t len; + char* buf; + FILE* stream; + + stream = open_memstream(&buf, &len); + table_printer_indent_and_reflow_text(stream, "one two three four", 7, 2, 4); + fclose(stream); + // strlen(src)=18 + first=2 + subsequent=(2x4) + newline=1 + ASSERT_INT_EQ(29, len); + ASSERT_STR_EQ(" one two\n three\n four\n", buf); + free(buf); + return 0; +} + +static int test_defaults(void) +{ + struct table_printer one; + table_printer_init(&one, stdout, 1); + table_printer_set_defaults(20, 2, 2); + struct table_printer two; + table_printer_init(&two, stderr, 2); + ASSERT_INT_EQ(80, one.max_width); + ASSERT_INT_EQ(4, one.left_indent); + ASSERT_INT_EQ(8, one.column_offset); + ASSERT_INT_EQ(1, one.left_width); + ASSERT_PTR_EQ(stdout, one.stream); + ASSERT_INT_EQ(20, two.max_width); + ASSERT_INT_EQ(2, two.left_indent); + ASSERT_INT_EQ(2, two.column_offset); + ASSERT_INT_EQ(2, two.left_width); + ASSERT_PTR_EQ(stderr, two.stream); + return 0; +} + +static int test_print_line(void) +{ + size_t len; + char* buf; + struct table_printer printer = { + .max_width = 20, + .left_indent = 2, + .left_width = 6, + .column_offset = 2, + }; + + printer.stream = open_memstream(&buf, &len); + table_printer_print_line(&printer, "left", "right"); + fclose(printer.stream); + ASSERT_STR_EQ(" left right\n", buf); + free(buf); + + printer.stream = open_memstream(&buf, &len); + table_printer_print_line(&printer, "left", "right side will wrap"); + fclose(printer.stream); + ASSERT_STR_EQ(" left right side\n" + " will wrap\n", buf); + free(buf); + return 0; +} + +static int test_print_fmtline(void) +{ + size_t len; + char* buf; + struct table_printer printer = { + .max_width = 20, + .left_indent = 2, + .left_width = 6, + .column_offset = 2, + }; + + printer.stream = open_memstream(&buf, &len); + table_printer_print_fmtline(&printer, "right", "left"); + fclose(printer.stream); + ASSERT_STR_EQ(" left right\n", buf); + free(buf); + + printer.stream = open_memstream(&buf, &len); + table_printer_print_fmtline(&printer, "right side will wrap", "left%d", 2); + fclose(printer.stream); + ASSERT_STR_EQ(" left2 right side\n" + " will wrap\n", buf); + free(buf); + return 0; +} + +int main() +{ + int r = 0; + RUN_TEST(test_reflow_text); + RUN_TEST(test_indent_and_reflow); + RUN_TEST(test_defaults); + RUN_TEST(test_print_line); + RUN_TEST(test_print_fmtline); + return r; +}