Getting the Terminal Width in C

Paul J. Lucas - May 2 '17 - - Dev Community

Introduction

One of my open-source projects is wrap that I use to reformat comments while editing source code. One of its command-line options is --width=n that allows you to specify the line width to wrap to. I wanted to add the ability to say --width=term that would use the current width of the terminal window in which wrap is running. So how do you get the current width of the terminal window? It turns out that a general solution is harder than you’d think.

One of the things that makes a general solution harder is that it must work even when none of standard input, output, or error are connected to a TTY because they’re redirected to a file or pipe. Indeed, the use-case for wrap is that it has to work when used as a filter from within vim.

Solution #1: ioctl()

If you do a Google search for how to solve this, many of the answers say to use ioctl(), something like:

unsigned get_term_width( void ) {
  struct winsize ws;
  if ( ioctl( STDIN_FILENO , TIOCGWINSZ, &ws ) != 0 &&
       ioctl( STDOUT_FILENO, TIOCGWINSZ, &ws ) != 0 &&
       ioctl( STDERR_FILENO, TIOCGWINSZ, &ws ) != 0 ) {
    fprintf( stderr, "ioctl() failed (%d): %s\n", errno, strerror( errno ) );
    return 0;
  }
  return ws.ws_col;
}
Enter fullscreen mode Exit fullscreen mode

This works only when at least one of standard input, output, or error is connected to a TTY which means that it doesn’t work for the use-case of wrap.

Solution #2: getmaxyx()

If you search a bit more, other answers say to use getmaxyx() that’s part of the curses library, something like:

unsigned get_term_width( void ) {
  initscr();                         // initialize curses
  int max_x, max_y;
  getmaxyx( stdscr, max_y, max_x );  // note: macro (no &) and max_x is second
  endwin();                          // teardown curses
  if ( max_x == -1 ) {
    fprintf( stderr, "getmaxyx() failed\n" );
    return 0;
  }
  return max_x;
}
Enter fullscreen mode Exit fullscreen mode

This is better in that it doesn’t matter whether at least one of standard input, output, or error is connected to a TTY. However, if you resize the window, it doesn’t notice the change and always returns the original size.

Solution #3: tgetnum()

If you search even more, yet other answers say to use tgetnum() that’s part of the terminfo library. However, to do that, you have to:

  1. Get the value of the TERM environment variable;
  2. Get that terminal’s data from the terminfo database via tgetent();
  3. Get the number of columns from tgetnum().

Something like:

unsigned get_term_width( void ) {
  char const *const term = getenv( "TERM" );
  if ( term == NULL ) {
    fprintf( stderr, "TERM environment variable not set\n" );
    return 0;
  }

  char term_buf[ 1024 ];
  switch ( tgetent( term_buf, term ) ) {
    case -1:
      fprintf( stderr, "tgetent() failed: terminfo database not found\n" );
      return 0;
    case 0:
      fprintf( stderr, "tgetent() failed: TERM=%s not found\n", term );
      return 0;
  } // switch

  int const cols = tgetnum( "co" );  // number of (co)lumns
  if ( cols == -1 ) {
    fprintf( stderr, "tgetnum() failed\n" );
    return 0;
  }
  return cols;
}
Enter fullscreen mode Exit fullscreen mode

Unfortunately, this suffers from the same problem as getmaxyx() in that it doesn’t notice window size changes.

Solution #4: tigetnum()

If you search for a really long time, you might find an answer that says to use tigetnum() that’s also part of the terminfo library. However, to do that, you have to:

  1. Get the value of the TERM environment variable;
  2. Get the path, e.g., /dev/tty001, of the current terminal via ctermid();
  3. open() that path;
  4. Get that terminal’s data from the terminfo database via setupterm();
  5. Get the number of columns from tigetnum() (not tgetnum()).

Something like:

unsigned get_term_width( void ) {
  char const *const term = getenv( "TERM" );
  if ( term == NULL ) {
    fprintf( stderr, "TERM environment variable not set\n" );
    return 0;
  }

  char const *const cterm_path = ctermid( NULL );
  if ( cterm_path == NULL || cterm_path[0] == '\0' ) {
    fprintf( stderr, "ctermid() failed\n" );
    return 0;
  }

  int tty_fd = open( cterm_path, O_RDWR );
  if ( tty_fd == -1 ) {
    fprintf( stderr,
      "open(\"%s\") failed (%d): %s\n", cterm_path, errno, strerror( errno )
    );
    return 0;
  }

  int cols = 0;
  int setupterm_err;
  if ( setupterm( (char*)term, tty_fd, &setupterm_err ) == ERR ) {
    switch ( setupterm_err ) {
      case -1:
        fprintf( stderr, "setupterm() failed: terminfo database not found\n" );
        goto done;
      case 0:
        fprintf( stderr,
          "setupterm() failed: TERM=%s not found in database or too generic\n",
          term
        );
        goto done;
      case 1:
        fprintf( stderr, "setupterm() failed: terminal is hardcopy\n" );
        goto done;
    } // switch
  }

  cols = tigetnum( (char*)"cols" );
  if ( cols < 0 )
    fprintf( stderr, "tigetnum() failed (%d)\n", cols );

done:
  if ( tty_fd != -1 )
    close( tty_fd );
  return cols < 0 ? 0 : cols;
}
Enter fullscreen mode Exit fullscreen mode

This solution (finally!) works — including noticing window size changes. That’s a lot of work to get something as “simple” as the current width of the terminal window. Welcome to C and Unix.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .