Getting the Terminal Width in C
Tags: c, terminal, tty, unix
Published: 2017-05-02 21:04:56

---
title: Getting the Terminal Width in C
published: true
description: How to get the width of the current terminal window in C.
tags: c, terminal, tty, unix
---

## Introduction

One of my open-source projects is [wrap](https://github.com/paul-j-lucas/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](http://www.vim.org).

## Solution #1: `ioctl()`

If you do a [Google search](https://www.google.com/search?q=terminal+width+c) for how to solve this, many of the answers say to use `ioctl()`, something like:

```c
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;
}
```

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](https://en.wikipedia.org/wiki/Curses_(programming_library)), something like:

```c
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;
}
```

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](https://en.wikipedia.org/wiki/Terminfo).  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:

```c
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;
}
```

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;
1. Get the path, e.g., `/dev/tty001`, of the current terminal via `ctermid()`;
2. `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:

```c
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;
}
```

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.