/*
 * 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 <stdio.h>
#include <termios.h>
#include <string.h>
#include <signal.h>
#include <stdlib.h>
#include <time.h>

#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);
}
