116

I've been looking for a way to get the terminal width from within my C program. What I keep coming up with is something along the lines of:

#include <sys/ioctl.h>
#include <stdio.h>

int main (void)
{
    struct ttysize ts;
    ioctl(0, TIOCGSIZE, &ts);

    printf ("lines %d\n", ts.ts_lines);
    printf ("columns %d\n", ts.ts_cols);
}

But everytime I try that I get

austin@:~$ gcc test.c -o test
test.c: In function ‘main’:
test.c:6: error: storage size of ‘ts’ isn’t known
test.c:7: error: ‘TIOCGSIZE’ undeclared (first use in this function)
test.c:7: error: (Each undeclared identifier is reported only once
test.c:7: error: for each function it appears in.)

Is this the best way to do this, or is there a better way? If not how can I get this to work?

EDIT: fixed code is

#include <sys/ioctl.h>
#include <stdio.h>

int main (void)
{
    struct winsize w;
    ioctl(0, TIOCGWINSZ, &w);

    printf ("lines %d\n", w.ws_row);
    printf ("columns %d\n", w.ws_col);
    return 0;
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
austin
  • 1,289
  • 4
  • 11
  • 10

8 Answers8

156

Have you considered using getenv() ? It allows you to get the system's environment variables which contain the terminals columns and lines.

Alternatively using your method, if you want to see what the kernel sees as the terminal size (better in case terminal is resized), you would need to use TIOCGWINSZ, as opposed to your TIOCGSIZE, like so:

struct winsize w;
ioctl(STDOUT_FILENO, TIOCGWINSZ, &w);

and the full code:

#include <sys/ioctl.h>
#include <stdio.h>
#include <unistd.h>

int main (int argc, char **argv)
{
    struct winsize w;
    ioctl(STDOUT_FILENO, TIOCGWINSZ, &w);

    printf ("lines %d\n", w.ws_row);
    printf ("columns %d\n", w.ws_col);
    return 0;  // make sure your main returns int
}
John T
  • 23,735
  • 11
  • 56
  • 82
  • 10
    yeah but the term width is not an environmental variable, its static to the term. – austin Jun 21 '09 at 01:42
  • 6
    It doesn't provide you the _current_ terminal size, if someone resizes the terminal during the program's execution. – C. K. Young Jun 21 '09 at 01:42
  • how to get sizes in pixels? I used `ws_xpixel` and `ws_ypixel`, But it just prints zeros ! – Debashish Mar 21 '17 at 19:30
  • 1
    @Debashish Depends. E.g. Linux doesn't support those fields at all. – melpomene Jul 05 '19 at 22:07
  • 2
    Although we can see the `LINES` and `COLUMNS` variables in the environment, they don't seem to be exported. So not accessible from your C program. – Alexis Wilke Jul 14 '19 at 21:59
  • 1
    Just stumbled upon this answer, and my jaw dropped when I realized that `getenv("COLUMNS")` works perfectly when running under watch(1). So now I have a set of fallbacks, all from the `TIOCWINSZ` ioctl, to getenv if not a tty, down to the classic ANSI escape "move cursor to 9999,9999 and the query cursor pos. The last one works on serial consoles for embedded systems :) – troglobit Dec 23 '20 at 08:54
  • You should check so that `ioctl` returns 0. Otherwise `w` may not be initialized. – Björn Lindqvist Jul 23 '23 at 18:45
19

This example is a bit on the lengthy side, but I believe it's the most portable way of detecting the terminal dimensions. This also handles resize events.

As tim and rlbond suggests, I'm using ncurses. It guarantees a great improvement in terminal compatability as compared to reading environment variables directly.

#include <ncurses.h>
#include <string.h>
#include <signal.h>

// SIGWINCH is called when the window is resized.
void handle_winch(int sig){
  signal(SIGWINCH, SIG_IGN);

  // Reinitialize the window to update data structures.
  endwin();
  initscr();
  refresh();
  clear();

  char tmp[128];
  sprintf(tmp, "%dx%d", COLS, LINES);

  // Approximate the center
  int x = COLS / 2 - strlen(tmp) / 2;
  int y = LINES / 2 - 1;

  mvaddstr(y, x, tmp);
  refresh();

  signal(SIGWINCH, handle_winch);
}

int main(int argc, char *argv[]){
  initscr();
  // COLS/LINES are now set

  signal(SIGWINCH, handle_winch);

  while(getch() != 27){
    /* Nada */
  }

  endwin();

  return(0);
}
gamen
  • 987
  • 6
  • 12
  • 5
    But is it really safe to call initscr and endwin from a signal handler? They're at least not listed among the async-signal-safe APIs in `man 7 signal` – nav Aug 06 '13 at 18:16
  • 1
    That's a good point @nav, I've *never* thought of that! Would a better solution perhaps be to have the signal handler raise a flag, and then perform the rest of the operations in the main loop? – gamen Oct 03 '13 at 09:55
  • 1
    @gamen, yes, that would be better ;) - also using sigaction instead of signal would be better too. – Bodo Thiesen Jul 30 '15 at 16:07
  • 1
    So are COLS and LINES global variables? – einpoklum Feb 28 '16 at 12:12
  • @einpoklum, Yes. ncurses has many "simply named" global variables for our enjoyment. https://www.tldp.org/LDP/lpg/node97.html – Alexis Wilke Jul 14 '19 at 22:36
  • 2
    @AlexisWilke: Including `OK` and `ERR`. How "kind" of them to help us fill that gap in our lives :-( – einpoklum Jul 15 '19 at 08:10
13
#include <stdio.h>
#include <stdlib.h>
#include <termcap.h>
#include <error.h>

static char termbuf[2048];

int main(void)
{
    char *termtype = getenv("TERM");

    if (tgetent(termbuf, termtype) < 0) {
        error(EXIT_FAILURE, 0, "Could not access the termcap data base.\n");
    }

    int lines = tgetnum("li");
    int columns = tgetnum("co");
    printf("lines = %d; columns = %d.\n", lines, columns);
    return 0;
}

Needs to be compiled with -ltermcap . There is a lot of other useful information you can get using termcap. Check the termcap manual using info termcap for more details.

Juliano
  • 39,173
  • 13
  • 67
  • 73
  • You can compile it with -lcurses too. – Kambus Apr 19 '11 at 10:12
  • I can't include termcap in Ubuntu 14.04 and can't find it in the repositories, either. :/ – Eric Sebasta Jun 11 '15 at 15:15
  • 2
    I know this comment comes 6 years after the fact, but please explain your magic number of 2048... – einpoklum Feb 20 '16 at 14:35
  • 1
    @einpoklum This is nearly three years later yet, but isn't it fairly clear that 2048 is just an arbitrary size for the buffer that "should probably be big enough" for whatever input string is going there? – Roflcopter4 Dec 12 '18 at 06:32
  • 2
    Actually, this answer makes too many assumptions to be correct. – Thomas Dickey May 08 '19 at 08:26
  • 2
    For anyone curious, the 2048 buffer size is explained in the GNU termcap documentation here: https://www.gnu.org/software/termutils/manual/termcap-1.3/html_mono/termcap.html#SEC4 There's also a lot of other stuff in there people reading this post may find useful. –  May 11 '20 at 22:10
  • I wonder if this works on Windows. i.e. if you can get termcap to work on Windows. – einpoklum Jul 23 '20 at 22:30
3

If you have ncurses installed and are using it, you can use getmaxyx() to find the dimensions of the terminal.

rlbond
  • 65,341
  • 56
  • 178
  • 228
3

To add a more complete answer, what I've found to work for me is to use @John_T's solution with some bits added in from Rosetta Code, along with some troubleshooting figuring out dependencies. It might be a bit inefficient, but with smart programming you can make it work and not be opening your terminal file all the time.

#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h> // ioctl, TIOCGWINSZ
#include <err.h>       // err
#include <fcntl.h>     // open
#include <unistd.h>    // close
#include <termios.h>   // don't remember, but it's needed

struct WinSize {
  size_t rows;
  size_t cols;
};

struct WinSize get_screen_size()
{
  struct winsize ws;
  int fd;

  fd = open("/dev/tty", O_RDWR);
  if(fd < 0 || ioctl(fd, TIOCGWINSZ, &ws) < 0) err(8, "/dev/tty");

  struct WinSize size;
  size.rows = ws.ws_row;
  size.cols = ws.ws_col;

  close(fd);

  return size;
}

If you make sure not to call it all but maybe every once in a while you should be fine, it should even update when the user resizes the terminal window (because you're opening the file and reading it every time).

If you aren't using TIOCGWINSZ see the first answer on this form https://www.linuxquestions.org/questions/programming-9/get-width-height-of-a-terminal-window-in-c-810739/.

Oh, and don't forget to free() the result.

lionkor
  • 562
  • 2
  • 18
iggy12345
  • 1,233
  • 12
  • 31
0

My Version Is An extermination of the ioctl approach I am not allocating memory and passing the struct back by value I believe so no memory leaks here

header file

#include <stdlib.h>
#include <sys/ioctl.h> // ioctl, TIOCGWINSZ

struct winsize get_screen_size();

unsigned short get_screen_width();
unsigned short get_screen_height();

void test_screen_size();

The implementation, I have also added a test function that fills in the terminal with a box padded by one char all the way around

/**
* Implementation of nos_utils signatures
*/

#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h> // ioctl, TIOCGWINSZ
#include <err.h>       // err
#include <fcntl.h>     // open
#include <unistd.h>    // close
//#include <termios.h>   // doesnt seem to be needed for this 
#include "nos_utils.h"

/**
 * @return  struct winsize   
 *  unsigned short int ws_row;
 *   unsigned short int ws_col;
 *   unsigned short int ws_xpixel;
 *   unsigned short int ws_ypixel;
 */
struct winsize get_screen_size() {
    struct winsize ws;
    int fd;

    fd = open("/dev/tty", O_RDWR);
    if (fd < 0 || ioctl(fd, TIOCGWINSZ, &ws) < 0) err(8, "/dev/tty");
    close(fd); // dont forget to close files
    return ws;
}

unsigned short get_screen_width() {
    struct winsize ws = get_screen_size();
    return ws.ws_col;
}

unsigned short get_screen_height() {
    struct winsize ws = get_screen_size();
    return ws.ws_row;
}

void test_screen_size() {
    struct winsize ws = get_screen_size();
//    unsigned short  h = ws.ws_row;
//    unsigned short  w = ws.ws_col;
    printf("The Teminal Size is\n rows: %zu  in %upx\n cols: %zu in %upx\n", ws.ws_row, ws.ws_ypixel, ws.ws_col,
           ws.ws_xpixel);
    
    unsigned short  h = get_screen_height();
    unsigned short  w = get_screen_width();
    h = h - 4; //for the 3 lines above + 1 fro new terminal line after :)
    for (unsigned short  i = 0; i < h; i++) {// for each row
        for (unsigned short  j = 0; j < w; j++) { // for each col
            //new line if we are last char
            if (j == w - 1) {
                printf(" \n");
            }//one space buffer around edge
            else if (i == 0 || i == h - 1 || j == 0) {
                printf(" ");
            } //the up arrows
            else if (i == 1) {
                printf("^");
            } else if (i == h - 2) {
                printf("v");
            } else if (j == 1) {
                printf("<");
            } else if (j == w - 2) {
                printf(">");
            } else {
                printf("#");
            }
        }//end col
    }//end row
}

int main(int argc, char** argv) {
    test_screen_size();
    return 0;
}

running test

Exo Flame
  • 97
  • 4
-1

Assuming you are on Linux, I think you want to use the ncurses library instead. I am pretty sure the ttysize stuff you have is not in stdlib.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
tim
  • 632
  • 1
  • 5
  • 10
  • well, what i'm doing isn't really worth setting up ncurses for – austin Jun 21 '09 at 01:42
  • 2
    ncurses is not in stdlib either. Both are standardized in POSIX, but the `ioctl` way is simpler and cleaner, because you don't have to initialize curses, etc. – Gandaro May 03 '14 at 22:17
-3

Here are the function calls for the already suggested environmental variable thing:

int lines = atoi(getenv("LINES"));
int columns = atoi(getenv("COLUMNS"));
merkuro
  • 6,161
  • 2
  • 27
  • 29
  • 13
    Environment variables are not reliable. These values are set by the shell, so they are not guaranteed to exist. Also, they won't be up to date if the user changes the terminal size. – Juliano Jun 21 '09 at 02:20
  • 1
    Many shells establish a handler for the `SIGWINCH` signal, so they can keep the variables up to date (they also need it so they'll do proper line wrapping in the input editor). – Barmar Jul 08 '14 at 16:11
  • 5
    They may well do that, but a program's environment won't be updated as it's running. – Functino Apr 10 '15 at 16:15
  • 1
    Of course, that code is very likely to crash since you do not test whether `getenv()` returns NULL or not and it does in my Linux terminal (because those variables are not exported.) Also even if the shell updates those variables, you would not see the changes while your program is running (not without you having your own `SIGWINCH` handler). – Alexis Wilke Jul 14 '19 at 23:13