Terminal window word wrapping in C

When writing a command-line interface (CLI) in C/C++, you may have experienced inconvenient line breaks right in the middle of a word or discontinued indentation. If you would like to display output tailored to the size of the terminal window, e.g. in help messages. Here I put together a couple of useful methods that I used to achieve that.

#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <unistd.h>

void fwrap(FILE* out, unsigned int w, unsigned int lm, char *prefix, char *ofmt, ...) {
  char empty[1] = {0}, line[w+1], *content;
  unsigned int bufsize, buflen, bufcur = 0, linelen, i, l = 0;
  for(bufsize=512;;bufsize+=512) {
    content = calloc(sizeof(char), bufsize);
    va_list argptr;
    va_start(argptr, ofmt);
    buflen = vsnprintf(content, bufsize, ofmt, argptr);
    va_end(argptr);
    if(buflen < bufsize) break;
  }
  if(w<lm+5) {
    if(prefix == NULL) {
      fprintf(out, "%s\n", content);
    }else{
      fprintf(out, "%s %s\n", prefix, content);
    }
    return;
  }
  if(prefix!=NULL && estrlen(prefix) > lm) {
    lm = estrlen(prefix);
  }
  while(bufcur<buflen) {
    if(isspace(content[bufcur])&&l>0) {
      bufcur++;
      continue;
    }
    for(i=0;ebstrlen(&content[bufcur],i)<w-lm;i++) {
      if(content[bufcur+i]=='\n'||content[bufcur+i]==0) {
        linelen = i;
        break;
      }
      if(content[bufcur+i]==' ') {
        linelen = i;
      }
    }
    if(linelen == 0) linelen = strlen(&content[bufcur]);
    memcpy(line, &content[bufcur], linelen);
    line[linelen] = 0;
    if(l == 0 && prefix != NULL) {
      fprintf(out, "%s%*.*s%s\n", prefix, estrlen(prefix)>lm ? 0 : lm-estrlen(prefix), estrlen(prefix)>lm ? 0 : lm-estrlen(prefix), " ", line);
    }else{
      fprintf(out, "%*s%.*s\n", lm, empty, w-lm, line);
    }
    bufcur+=linelen;
    l++;
  }
  free(content);
}

This function will output the format string ofmt, using any following variable arguments ..., into out but break lines after a maximum line length of w characters. lm specifies the width of the left margin in character spaces (use 0 for no margin). Right margin can be achieved by decreasing output width w. If prefix is not NULL, the function prints that string within the left margin of the first line.

You may have noted the functions estrlen and ebstrlen. Those are necessary to determine the visible length of a string (any character that is not a line break or an ASCII format sequence). ebstrlen returns the maximum number of bytes (chars) so that the visible length does not exceed ub. If ub = -1 then the visible length of the entire string is returned. estrlen is a shorthand version for that. Note that substrings can be evaluated by passing the address of a subcharacter rather than the entire string as str.

size_t ebstrlen(const char *str, size_t ub) {
  size_t i, ret;
  unsigned short skip = 0;
  for(i=0,ret=0;str[i]!=0&&(ub<0||i<ub);i++) {
    if(str[i]=='\e'&&str[i+1]=='[')
      skip = 1;
    if(skip && str[i] == 'm') {
      skip = 0;
      continue;
    }
    if(skip)
      continue;
    switch(str[i]) {
      case '\t': ret+=8; break;
      case '\n': break;
      case '\r': break;
      default: ret++; break;
    }
  }
  return ret;
}

size_t estrlen(const char *str) {
  return ebstrlen(str, -1);
}

You can then use fwrap as follows without left margin and prefix:

fwrap(stdout, 20, 0, NULL, "This is %s. If you find this useful, please feel free to %s!", "some sample output", "leave me a comment");
This is some sample
output. If you find
this useful, please
feel free to leave
me a comment!

Or with a margin and prefix:

fwrap(stdout, 60, 10, "Test:", "This is %s. If you find this useful, please feel free to %s!", "some sample output", "leave me a comment");
Test:     This is some sample output. If you find this
          useful, please feel free to leave me a comment!

I intentionally require output width as an argument for the function. One reason is that this allows you to specify a right margin in addition to the left margin. Another reason is that if you wish to automatically adjust the output to the width of the terminal window, there might be different ways how to achieve this that. The one I used and usually works is that at the beginning of the application, I query ioctl for the dimensions of stdout like this:

#include <sys/ioctl.h>

struct winsize w;

w.ws_col = 80;
ioctl(STDOUT_FILENO, TIOCGWINSZ, &w);

This will set 80 as the default output width and then try to set the dimensions by using ioctl. If it fails, 80 will remain as the value for w.ws_col. You can then use the terminal width in the function call as follows:

fwrap(stdout, w.ws_col, 10, "Test:", "This is %s. If you find this useful, please feel free to %s!", "some sample output", "leave me a comment");

When using the function, please note the following:

  1. If your output width is too narrow (less than 5 characters plus requested left margin), the formatted string will be written into the output file (or standard output) without any further word wrapping.
  2. If your formatted string contains line breaks, this is respected by the function and interpreted as a forced line break. So whenever it hits a line break character, it will not wait until it reaches the maximum width but begin a new line right away.
  3. Any white space at the beginning of any line is truncated.
  4. The word wrapping is only done once at the time the function is called. If you determined the width by using ioctl, only the width at that time is used. If your used changed the width of the terminal window afterwards (either between calling ioctl and fwrap or after calling fwrap), the output might look very strange. Especially if the window is more narrow, there will be additional line breaks that mess up the formatting.
  5. That’s why this function is not helpful for creating static files if the width of the viewer is not known.
  6. That’s also why this function is not intended for creating machine-readable output.

So, if you have found this helpful, as mentioned in the sample output above, leave me a comment!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.