/*
/* 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); }