/* * wsize.c - 2000-01-04 * * written by : Stephen J. Friedl * Software Consultant * Tustin, California USA * steve@unixwiz.net * * -------------------------------------------------------------- * This is free software in the public domain, and it has no * restrictions of any kind. Have fun! * -------------------------------------------------------------- * * Most screen-oriented UNIX programs require that the screen be a * standard size (24x80), and if they are different from this, the * environment variables $LINES and $COLUMNS must be set to match. * If the expected size of the screen doesn't match the actual size, * it's usually a big mess. * * The telnet protocol can pass window-size changes via the out- * of-band methods, and some software (mostly on Linux or Sun) * understand this and size things correctly. This is usually done * with the SIGWINCH signal for Window Change. * * However, not everybody plays this game correctly, and it gets * very tiring to type * * $ export LINES=30 COLUMNS=100 * * every time we login or change the screen size. This program is * meant to address this. * * This progarm queries the terminal as to its current size and sets * the proper variables as needed. It works by sending the cursor * to the lower right side of the screen and asking the terminal * for its current cursor position. Our movement to the lower right * is via outrageously high values for row and column (255), and * we rely on the terminal to make a best effort *without wrapping. * * Once the terminal tells us the position, we restore the cursor * to its previous position. * * The cursor report is done with ESC [ 6 n, and the response is * ESC [ 45 ; 1 R (row first, then column). We wait for only a short * time collecting the response, and we parse it to determine the * # of rows and columns. If we don't get a response in time, we * report this to the caller and don't change the current values * of $LINES or $COLUMNS. * * To use this: * * eval `wsize` * * We normally add an alias in the /etc/profile: * * alias wsize=`eval /usr/local/bin/wsize` * * This was mainly written to support Van Dyke Technologies' "CRT" * and "Secure CRT" products and has not been tested with any other * products. This is a killer good telnet emulator that we've been * using for YEARS, and we can't find anything about it not to * like. http://www.vandyke.com * * Feedback and corrections welcome. * * BUILDING THIS PROGRAM * --------------------- * * This program compiles cleanly on all platforms we've tried it on, * including Linux, Sun, and several versions of SCO UNIX. We are happy * to accept porting tips from anybody who cares to offer them. * * Compile this program with: * * $ cc wsize.c -o wsize * * and put "wsize" in some public place (say, /usr/local/bin). */ #include #include #include #include #include #include #ifndef TRUE # define TRUE 1 # define FALSE 0 #endif static const char Version[] = "wsize - Stephen Friedl - steve@unixwiz.net"; /*------------------------------------------------------------------------ * "save_tty" is the set of termio paramters ("stty params") that were in * effect when this program started. We need to temporarily enter char-at- * a-time mode to read our responses, and we put it all back when done. The * "save_tty_valid" is TRUE if we're allowed to make this update. */ static struct termios save_tty; static int save_tty_valid = 0; static int read_fd = -1; static void on_signal(int signo); static void cleanup(void); int main(int argc, char **argv) { struct termios new_tty; char ibuf[100], *iptr = ibuf, *imax = ibuf + sizeof ibuf - 1; int nr, maxloops, ncols = -1, nrows = -1; /*---------------------------------------------------------------- * Make sure that our input is actually a terminal: otherwise this * test is pointless. */ read_fd = fileno(stdin); if ( ! isatty(read_fd) ) { fprintf(stderr, "ERROR: input is not a terminal!\n"); exit(EXIT_FAILURE); } fprintf(stderr, "\0337"); /* save current position */ /*---------------------------------------------------------------- * Set up the input to properly handle our responses from the * terminal. We turn off echo so the response from the termianal * doesn't display, and ICANON mode must be off so we can read * a character at a time. It is a serious error if we cannot get * the tty stats. */ if ( tcgetattr(read_fd, &save_tty) != 0 ) { fprintf(stderr, "ERROR: cannot fetch tty stats\n"); exit(1); } save_tty_valid = TRUE; new_tty = save_tty; new_tty.c_lflag &= ~(ICANON); new_tty.c_lflag &= ~(ECHO|ECHOE|ECHOK|ECHONL); new_tty.c_cc[VMIN] = 1; new_tty.c_cc[VTIME] = 1; if ( tcsetattr(read_fd, TCSANOW, &new_tty) != 0 ) { fprintf(stderr, "ERROR: cannot set tty stats\n"); exit(1); } /*---------------------------------------------------------------- * Query the terminal */ fprintf(stderr, "\033[r"); /* clear scrolling region */ fprintf(stderr, "\033[255;255H"); /* go to lower right of screen */ fprintf(stderr, "\033[6n"); /* query current cursor location */ /*---------------------------------------------------------------- * Prepare to catch our signals. We treat both an interrupt and an * alarm as essentially the same thing: fatal errors. */ { struct sigaction act; act.sa_handler = on_signal; sigemptyset(&act.sa_mask); act.sa_flags = 0; sigaction(SIGINT, &act, (struct sigaction *) 0); sigaction(SIGALRM, &act, (struct sigaction *) 0); } /*---------------------------------------------------------------- * Now go get our response from the terminal. We have a sanity * check of only so many loops, plus we plant a timeout in case we * get nothing. * * NOTE: the timeout is *required* because we have VMIN/VTIME * settings that will not return if there is no input. In the * absense of this alarm, this program will hang. */ alarm(1); maxloops = 20; while ( --maxloops > 0 && (nr = read(read_fd, iptr, (int)(imax - iptr))) > 0 ) { char *p; /*-------------------------------------------------------- * First manager our read buffer. We always see to it that * we drop a NUL byte at the end, then see if this is worth * parsing. We require that we find the ESCape character * in the buffer, then see what's after it. * * If this read returned zero, there is nothing for us to * even try parsing, so we just skip to another loop. */ if ( nr <= 0 ) continue; iptr += nr; /* advance pointer */ iptr[-1] = '\0'; /* always NUL terminate */ if ( (p = strchr(ibuf, '\033')) == 0 ) continue; ncols = -1; nrows = -1; if ( sscanf(p, "\033[%d;%dR", &nrows, &ncols) == 2 ) { break; } } alarm(0); /* disable our alarm */ cleanup(); /*---------------------------------------------------------------- * If we actually read rows & columns from the terminal, we should * write the environment variables to the user. Write to standard * error only if standard output is not a terminal. */ if ( ncols > 0 && nrows > 0 ) { printf("COLUMNS=%d LINES=%d; export COLUMNS LINES\n", ncols, nrows); if ( ! isatty(fileno(stdout)) ) fprintf(stderr, "COLUMNS=%d LINES=%d\n", ncols, nrows); } else { fprintf(stderr, "*no window size seen\n"); } exit(EXIT_SUCCESS); } /* * cleanup() * * Do what we can to put the terminal back like we found it. This * includes resetting the stty parameters and moving the cursor * back to its saved location. Note that we are only allowed to * perform *one* restore of the tty stats. */ static void cleanup(void) { int is_valid = save_tty_valid; save_tty_valid = FALSE; if ( is_valid ) { tcsetattr(read_fd, TCSANOW, &save_tty); } /* restore cursor position */ fprintf(stderr, "\0338"); } /* * on_signal() * * This is our signal handler that processes both the timeout and * the interrupt. In both cases we do as much cleanup as we can * and print a message letting the user know what happened. */ static void on_signal(int signo) { const char *msg; cleanup(); switch (signo) { case SIGINT: msg = " (interrupt)"; break; case SIGALRM: msg = " (alarm"; break; default: msg = ""; break; } fprintf(stderr, "\n\r*INTERRUPTED by signal %d%s\n", signo, msg); exit(EXIT_FAILURE); }