/*

resize.c

*/

Uwe Ohse

available software

resize.c

/*

this program demonstrates how complicated a simple task can be:
    the inquiry of the remote terminals window size.

I didn't bother about portability for this example. This source compiles
under linux, and it should not make much trouble to compile it almost
everythere else.

Some background:

* there are three different terminal size:
  - $LINES and $COLUMNS. Ignored here.
  - the `real' size of the window: this is what the terminal or terminal
    emulator thinks about the window size (this might be a real
    terminal, a virtual terminal, a terminal program as Rufus, Telis,
    Telemate or ZTerm, or even xterm).
    This is what the terminal really supports.
  - the `kernel' size. This is what normal programs get from the kernel
    if they ask him about the terminal size. You can change or get the
    size with ioctl().
    If this size is smaller than the real this: fine, but the program
    will not use the whole window.
    It this size it too large: bad, parts of the screen will be scrolled
    out.

* there is a program `resize' (/usr/bin/X11/resize) which can do the
  same job as this hack here.

* i developed this for a BBS system, which used to call gopher as an
  external program. Unfortunately gopher highly depends on the right
  `kernel' window size. needless to say that only few users did tell the
  box the real size.  Well, `resize_terminal' does get it right for
  them.  (just for the records: i had a hard time writing
  resize_terminal. Not because there is to few documentation available,
  but because i had *none*, and not to much experience with terminals
  either. I still don't have any documentation)

Okay, here is what needs to be done:

1. put the terminal in noncanonical mode 
   (canonical mode won't work).
2. turn off any scroll regions.
   (so step 3 can work)
   oh, btw: there is now way to restore the scroll region.
3. put the cursor really far away (999,999).
   (`resize' doesn't do this, but `resize' just deals
   with xterm, and not with terminal programs)
4. tell (through sending an escape sequence) the terminal 
   to send the size
5. read & parse the terminals reply. Be careful not to 
   hang (==provide a timeout).
no, this is not a joke. 

It's important to catch some signals, so canonical mode
is restored even if the user hit ^C.

If you want to use this routines in your programs:
- feel free. It's GPL, remember.
- perhaps tell your users not to press a key while getting the terminal
  size.
- don't forget $LINES and $COLUMNS. Some program might need them.
- there are two ways if getting/setting the kernels terminal size
  (TIOCGSIZE & TIOCGWINSZ).  Portability is funny ...

Feel free to ask me about it. Feel free to send me
improvements.

*/

/*
    Copyright (C) 1996 1997 Uwe Ohse

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

    Contact: uwe@tirka.gun.de, Uwe Ohse @ DU3 (mausnet)
*/

/* need sighandler_t */
#define _GNU_SOURCE

#include <ctype.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <limits.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <signal.h>
#include <termios.h>

#include <termcap.h>

/* put this into a header file if you want */
#define RESIZE_MODE_NORMAL    0
#define RESIZE_MODE_INQUIRE   1
#define RESIZE_MODE_FORCE_VT  2
#define RESIZE_ANS_OK         0
#define RESIZE_ANS_NOT_TERM   1
#define RESIZE_ANS_NOT_CAP    2
#define RESIZE_ANS_FAILED     3
int io_resize_terminal(int fdin, int fdout, int mode, const char *);
/* end of header file */


static void alarm_handler(int signo);
static int read_answer(int fdin, const char *tmpl, char *buf, size_t max);
static void change_term_size (int fd, int x, int y);
static int get_term_size (int fd, int *x, int *y);

static int got_alarm=0;

/*
 * mode:
 * RESIZE_MODE_NORMAL   - normal workmode: 
 *     `resize' if terminal has capabilities.
 * RESIZE_MODE_INQUIRE  - inquire capabilities.
 * RESIZE_MODE_FORCE_VT - force mode: 
 *     force ANSIvt100 escape sequences.
 * Return, normal ans force modus:
 * RESIZE_ANS_OK       - ok.
 * RESIZE_ANS_NOT_TERM - this is not a terminal
 * RESIZE_ANS_NOT_CAP  - not possible, terminal lacks
       capability (not in force mode)
 * RESIZE_ANS_FAILED   - execution failed
 * Return, inquire mode:
 * RESIZE_ANS_OK       - ok.
 * RESIZE_ANS_NOT_TERM - this is not a terminal
 * RESIZE_ANS_NOT_CAP  - not possible, terminal lacks
       capability (try force mode)
 */
int
io_resize_terminal(int fdin, int fdout, int mode, const char *termname)
{
  const char *request=NULL;
  const char *report=NULL;
  const char *cursor=NULL;
  const char *store=NULL;
  const char *restore=NULL;
  const char *scrollregion=NULL;
  char buf[1024];
  char *p;
  struct stat st0;
  struct stat st1;
  char rbuf[128];
  int y=-1,x=-1;
  int ok=0;
  struct termios saved,work;
  
  sighandler_t old_quit;
  sighandler_t old_term;
  sighandler_t old_int;
  sighandler_t old_hup;

  if (!isatty(fdin) || !isatty(fdout))
    return RESIZE_ANS_NOT_TERM;

  /* are stdin/stdout the same device? */
  if (fstat(fdin,&st0) || fstat(fdout,&st1) 
    || !S_ISCHR(st0.st_mode) || !S_ISCHR(st1.st_mode))
    return RESIZE_ANS_NOT_TERM;
  if (st0.st_rdev!=st1.st_rdev)
    return RESIZE_ANS_NOT_TERM;

  if (!termname)
    termname="unknown";

  p=buf;
  request=tgetstr("u7",&p);
  report=tgetstr("u6",&p);
  cursor=tgetstr("cm",&p);
  store=tgetstr("sc",&p);
  restore=tgetstr("rc",&p);
  scrollregion=tgetstr("cs",&p);

  /* ncurses are braindead: to keep the termcap entry size below
     * 1024 bytes they don't provice u7/u6.
   */
  if (!strcasecmp(termname,"linux") 
  || !strcasecmp(termname,"console")
  || !strcasecmp(termname,"cons80x25")
  || !strcasecmp(termname,"cons80x30")
  || !strcasecmp(termname,"cons80x40")
  || !strcasecmp(termname,"cons80x50")
  || !strcasecmp(termname,"con80x25")
  || !strcasecmp(termname,"con80x30")
  || !strcasecmp(termname,"con80x40")
  || !strcasecmp(termname,"con80x50")
  )
  {
    if (!report)
      report="\E[%d;%dR";
    if (!request)
      request="\E[6n";
  }
  /* for some terminals we know the escape codes, even if
     * the termcap doesn't 
   */
  if (!report && (!strcasecmp(termname,"vt100") || strstr(termname,"xterm")))
    report="\E[%d;%dR";
  if (!request && (!strcasecmp(termname,"vt100") || strstr(termname,"xterm")))
    request="\E[6n";
  if (!report && mode==2)
    report="\E[%d;%dR";
  if (!request && mode==2)
    request="\E[6n";
  if (!cursor && mode==2)
    cursor="\E[%d;%dH";

  if (!request || !report || !cursor)
    return RESIZE_ANS_NOT_CAP;
  if (mode==1)
    return RESIZE_ANS_OK;

    if (tcgetattr(fdin,&work))
        return 0;
  saved=work;
    work.c_lflag &= ~(ICANON);
    work.c_lflag &= ~(ECHO);

  old_int=signal(SIGINT,SIG_IGN);
  old_quit=signal(SIGQUIT,SIG_IGN);
  old_hup=signal(SIGHUP,SIG_IGN);
  old_term=signal(SIGTERM,SIG_IGN);

    if (tcsetattr(fdin,TCSADRAIN,&work)) {
    signal(SIGINT,old_int);
    signal(SIGQUIT,old_quit);
    signal(SIGHUP,old_hup);
    signal(SIGTERM,old_term);
        return 0;
  }

  if (store && restore)
    write(fdout,store,strlen(store));

  /* turn scroll region off */
  if (scrollregion)
  {
    char *tmp=tparam(scrollregion,NULL,0,998,998);
    if (strcmp(tmp,"\033[999;999r")==0) /* a little hack: this *is* ANSI */
      write(fdout,"\033[r",3); /* turn scroll region off */
    else
      write(fdout,tmp,strlen(tmp));
    free(tmp);
  }

  /* put cursor far, far away */
  p=tgoto(cursor,998,998);
  write(fdout,p,strlen(p));

  /* ask terminal: `where is the cursor?' */
  write(fdout,request,strlen(request));

  /* get terminal answer */
  x=80;
  y=25;
  if (read_answer(fdin, report, rbuf, sizeof(rbuf)))
  {
    if (*rbuf)
    {
      sscanf(rbuf,report,&y,&x);
      if (x>0 && y>0 && x<998 && y<998)
      {
        /* tell kernel about screen size */
        change_term_size(0,x,y);
        ok=1;
      }
      /* else out of range */
    }
     /* else no answer */
  }
  /* else timed out */

  /* set scroll region to exactly one screen */
  if (scrollregion && ok)
  {
    char *tmp=tparam(scrollregion,NULL,0,0,y-1);
    write(fdout,tmp,strlen(tmp));
    free(tmp);
  }

  if (store && restore)
    write(fdout,restore,strlen(restore));
    tcsetattr(fdin,TCSADRAIN,&saved);

  signal(SIGINT,old_int);
  signal(SIGQUIT,old_quit);
  signal(SIGHUP,old_hup);
  signal(SIGTERM,old_term);
  if (ok)
    return RESIZE_ANS_OK;
  return RESIZE_ANS_FAILED;
}

static void
alarm_handler(int signo)
{
  got_alarm=1;
}

static int
read_answer(int fdin, const char *tmpl, char *buf, size_t max)
{
  void (*old_alarm)(int);
  char last;
  char c=0;
  char *p;
  int cnt;

  old_alarm=signal(SIGALRM,alarm_handler);
  got_alarm=0;
  alarm(1);

    last = tmpl[strlen(tmpl) - 1];
  cnt=0;
  p=buf;
    while (read(fdin,&c,1)!=-1) 
  {
    if (got_alarm)
      break;
    if (!c) /* telnet 0 bytes ... */
      continue;
    *p++=c;
    if (++cnt==max-1)
      break;
    if (c==last)
      break;  
  }
  alarm(0);
  signal(SIGALRM,old_alarm);
  *p=0;
  if (last==c)
    return 1;
  return 0;
}

/*
 * change kernel terminal size.
 */
static void
change_term_size (int fd, int x, int y)
{
#ifdef TIOCGSIZE
    struct ttysize win;

#elif defined(TIOCGWINSZ)
    struct winsize win;

#endif

#ifdef TIOCGSIZE
    if (ioctl (fd, TIOCGSIZE, &win))
        return;
    if (y && y>24)
        win.ts_lines = y;
    else
        win.ts_lines = 24;
    if (x && x>80)
        win.ts_cols = x;
    else
        win.ts_cols = 80;
    ioctl (fd, TIOCSSIZE, &win);
#elif defined TIOCGWINSZ
    if (ioctl (fd, TIOCGWINSZ, &win))
        return;
    if (y && y >24)
        win.ws_row = y;
    else
        win.ws_row = 24;
    if (x && x>80)
        win.ws_col = x;
    else
        win.ws_col = 80;
    ioctl (fd, TIOCSWINSZ, &win);
#endif
}

/*
 * inquire actual terminal size (this it what the 
 * kernel thinks - not was the user on the over end
 * of the phone line has really).
 */
static int
get_term_size (int fd, int *x, int *y)
{
#ifdef TIOCGSIZE
    struct ttysize win;

#elif defined(TIOCGWINSZ)
    struct winsize win;

#endif

#ifdef TIOCGSIZE
    if (ioctl (fd, TIOCGSIZE, &win))
        return 0;
    if (y)
        *y=win.ts_lines;
    if (x)
        *x=win.ts_cols;
#elif defined TIOCGWINSZ
    if (ioctl (fd, TIOCGWINSZ, &win))
        return 0;
    if (y)
        *y=win.ws_row;
    if (x)
        *x=win.ws_col;
#else
    {
        const char *s;
        s=getenv("LINES");
        if (s)
            *y=strtol(s,NULL,10);
        else
            *y=25;
        s=getenv("COLUMNS");
        if (s)
            *x=strtol(s,NULL,10);
        else
            *x=80;
    }
#endif
    return 1;
}




int main(int argc, char **argv) 
{
  int ret;
  int c;
  int inquire=0;
  int force=0;
  const char *term=NULL;
  int verbose=0;
  int x,y;

  opterr = 0;

  while ((c = getopt (argc, argv, "ift:v")) != -1) {
    switch(c)
    {
    case 'i': inquire=1; break;
    case 'f': force=1; break;
    case 't': term=optarg; break;
    case 'v': verbose++; break;
    case '?':
        if (isprint (optopt))
             fprintf (stderr, "Unknown option `-%c'.\n", optopt);
        else
             fprintf (stderr, "Unknown option character `\\x%x'.\n", optopt);
        exit(1);
    }
  }

  if (!term)
    term=getenv("TERM");
  if (!term) {
    fprintf(stderr,"TERM not set, using `linux'\n");
    term="linux";
  }
  ret=tgetent(NULL,term);
  if (ret < 0) {
    fprintf(stderr,"tgetent failed: no termcap data base\n");
    exit(1);
  } else if (ret == 0) {
    fprintf(stderr,"tgetent failed: no terminal type `%s'\n",term);
    exit(1);
  }

  /* no, do not restart system calls. stupid BSD signals */
  siginterrupt(SIGALRM,1);

  if (verbose) {
    if (!get_term_size(STDIN_FILENO,&x,&y)) {
      fprintf(stderr,"warning: failed to get old terminal size\n");
    } else {
      fprintf(stderr,"old terminal size: %d * %d\n",x,y);
    }
  }

  ret=io_resize_terminal(0,0,RESIZE_MODE_INQUIRE,term);
  if (ret!=RESIZE_ANS_OK) {
      fprintf(stderr, "terminal inquire failed: ");
      switch(ret)
      {
      case RESIZE_ANS_NOT_TERM:
         fprintf(stderr,"no terminal\n"); 
         break;
      case RESIZE_ANS_NOT_CAP:
         fprintf(stderr,"terminal too stupid\n"); 
         break;
      }
      if (!force)
         exit(1);
      fprintf(stderr,"forcing resize() with ANSI/vt100 terminal capabilities\n");
  }
  if (inquire) {
    exit(0);
  }


  if (ret==RESIZE_ANS_OK)
    ret=io_resize_terminal(STDIN_FILENO,STDOUT_FILENO,RESIZE_MODE_NORMAL,term);
  else
    ret=io_resize_terminal(STDIN_FILENO,STDOUT_FILENO,RESIZE_MODE_FORCE_VT,term);
  if (ret != RESIZE_ANS_OK) {
      fprintf(stderr, "terminal resize failed: ");
      switch(ret)
      {
      case RESIZE_ANS_NOT_TERM:
         fprintf(stderr,"no terminal\n"); 
         break;
      case RESIZE_ANS_NOT_CAP:
         fprintf(stderr,"terminal too stupid\n"); 
         break;
      case RESIZE_ANS_FAILED:
         fprintf(stderr,"bad luck\n"); 
         break;
      }
      exit(1);
  }
  if (verbose)
    printf("ok\n");
  if (verbose) {
    if (!get_term_size(STDIN_FILENO,&x,&y)) {
      fprintf(stderr,"warning: failed to get new terminal size\n");
    } else {
      fprintf(stderr,"new terminal size: %d * %d\n",x,y);
    }
  }
  exit(0);
}

Please send comments on these web pages and questions to uwe@ohse.de.