diff --git a/app/meson.build b/app/meson.build index f08d4cab..0e33c084 100644 --- a/app/meson.build +++ b/app/meson.build @@ -187,6 +187,7 @@ if get_option('buildtype') == 'debug' 'tests/test_cli.c', 'src/cli.c', 'src/options.c', + 'src/util/strbuf.c', 'src/util/str_util.c', ]], ['test_clock', [ @@ -196,6 +197,7 @@ if get_option('buildtype') == 'debug' ['test_control_msg_serialize', [ 'tests/test_control_msg_serialize.c', 'src/control_msg.c', + 'src/util/strbuf.c', 'src/util/str_util.c', ]], ['test_device_msg_deserialize', [ @@ -211,6 +213,7 @@ if get_option('buildtype') == 'debug' ]], ['test_strutil', [ 'tests/test_strutil.c', + 'src/util/strbuf.c', 'src/util/str_util.c', ]], ] diff --git a/app/src/util/str_util.c b/app/src/util/str_util.c index 287c08de..37f5a8de 100644 --- a/app/src/util/str_util.c +++ b/app/src/util/str_util.c @@ -1,9 +1,11 @@ #include "str_util.h" +#include #include #include #include #include +#include "util/strbuf.h" #ifdef _WIN32 # include @@ -209,3 +211,76 @@ utf8_from_wide_char(const wchar_t *ws) { } #endif + +char *wrap_lines(const char *input, unsigned columns, unsigned indent) { + struct sc_strbuf buf; + + // The output string should not be a lot longer than the input string (just + // a few '\n' added), so this initial capacity should almost always avoid + // internal realloc() in string buffer + size_t cap = strlen(input) * 3 / 2; + + if (!sc_strbuf_init(&buf, cap)) { + return false; + } + + assert(indent < columns); + +#define APPEND(S,N) if (!sc_strbuf_append(&buf, S, N)) goto error +#define APPEND_CHAR(C) if (!sc_strbuf_append_char(&buf, C)) goto error +#define APPEND_N(C,N) if (!sc_strbuf_append_n(&buf, C, N)) goto error +#define APPEND_INDENT() if (indent) APPEND_N(' ', indent) + + APPEND_INDENT(); + + // The last separator encountered, it must be inserted only conditionnaly, + // depending on the next token + char pending = 0; + + // col tracks the current column in the current line + size_t col = indent; + while (*input) { + size_t sep_idx = strcspn(input, "\n "); + bool wrap = col + sep_idx > columns; + + char sep = input[sep_idx]; + if (sep == ' ') + sep = ' '; + + if (wrap) { + APPEND_CHAR('\n'); + APPEND_INDENT(); + col = indent; + } else if (pending) { + APPEND_CHAR(pending); + ++col; + if (pending == '\n') + { + APPEND_INDENT(); + col = indent; + } + } + + if (sep_idx) { + APPEND(input, sep_idx); + col += sep_idx; + } + + pending = sep; + + input += sep_idx; + if (*input != '\0') { + // Skip the separator + ++input; + } + } + + if (pending) + APPEND_CHAR(pending); + + return buf.s; + +error: + free(buf.s); + return NULL; +} diff --git a/app/src/util/str_util.h b/app/src/util/str_util.h index 361d2bdd..e78be041 100644 --- a/app/src/util/str_util.h +++ b/app/src/util/str_util.h @@ -62,4 +62,8 @@ char * utf8_from_wide_char(const wchar_t *s); #endif +// Wrap input lines at words boundaries (spaces) so that they fit in 'columns' +// columns, left-indented by 'indent' spaces +char *wrap_lines(const char *input, unsigned columns, unsigned indent); + #endif diff --git a/app/tests/test_strutil.c b/app/tests/test_strutil.c index dfd99658..9ad680a6 100644 --- a/app/tests/test_strutil.c +++ b/app/tests/test_strutil.c @@ -299,6 +299,45 @@ static void test_strlist_contains(void) { assert(strlist_contains("xyz", '\0', "xyz")); } +static void test_wrap_lines(void) { + const char *s = "This is a text to test line wrapping. The lines must be " + "wrapped either at a space or a line break.\n" + "\n" + "This rectangle must remains a rectangle because it is " + "drawn in lines having lengths lower than the specified " + "number of columns:\n" + " +----+\n" + " | |\n" + " +----+\n"; + + // |---- 1 1 2 2| + // |0 5 0 5 0 3| <-- 24 columns + const char *expected = " This is a text to\n" + " test line wrapping.\n" + " The lines must be\n" + " wrapped either at a\n" + " space or a line\n" + " break.\n" + " \n" + " This rectangle must\n" + " remains a rectangle\n" + " because it is drawn\n" + " in lines having\n" + " lengths lower than\n" + " the specified number\n" + " of columns:\n" + " +----+\n" + " | |\n" + " +----+\n"; + + char *formatted = wrap_lines(s, 24, 4); + assert(formatted); + + assert(!strcmp(formatted, expected)); + + free(formatted); +} + int main(int argc, char *argv[]) { (void) argc; (void) argv; @@ -317,5 +356,6 @@ int main(int argc, char *argv[]) { test_parse_integers(); test_parse_integer_with_suffix(); test_strlist_contains(); + test_wrap_lines(); return 0; }