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;
}
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;
}
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:
- Get the value of the
TERM
environment variable; - Get that terminal’s data from the terminfo database via
tgetent()
; - 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;
}
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:
- Get the value of the
TERM
environment variable; - Get the path, e.g.,
/dev/tty001
, of the current terminal viactermid()
; -
open()
that path; - Get that terminal’s data from the terminfo database via
setupterm()
; - Get the number of columns from
tigetnum()
(nottgetnum()
).
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;
}
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.