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..b4e7cac4 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,81 @@ utf8_from_wide_char(const wchar_t *ws) { } #endif + +char *sc_str_wrap_lines(const char *input, unsigned columns, unsigned indent) { + assert(indent < columns); + + struct sc_strbuf buf; + + // The output string should not be much longer than the input string (just + // a few '\n' added), so this initial capacity should hopefully almost + // always avoid internal realloc() in string buffer + size_t cap = strlen(input) * 3 / 2; + + if (!sc_strbuf_init(&buf, cap)) { + return false; + } + +#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 "); + size_t new_col = col + sep_idx; + if (pending == ' ') { + // The pending space counts + ++new_col; + } + bool wrap = new_col > 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..344522c9 100644 --- a/app/src/util/str_util.h +++ b/app/src/util/str_util.h @@ -62,4 +62,12 @@ char * utf8_from_wide_char(const wchar_t *s); #endif +/** + * Wrap input lines to fit in `columns` columns + * + * Break input lines at word boundaries (spaces) so that they fit in `columns` + * columns, left-indented by `indent` spaces. + */ +char *sc_str_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..2d23176e 100644 --- a/app/tests/test_strutil.c +++ b/app/tests/test_strutil.c @@ -299,6 +299,44 @@ 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 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 at a space\n" + " or a line 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 = sc_str_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 +355,6 @@ int main(int argc, char *argv[]) { test_parse_integers(); test_parse_integer_with_suffix(); test_strlist_contains(); + test_wrap_lines(); return 0; }