1 /*
   2  * Copyright (c) 2013 Joyent Inc., All rights reserved.
   3  *
   4  * This tool uses terminal control sequences to attempt to measure the size
   5  * of the controlling terminal.  Either the tool will fail with a non-zero
   6  * exit status, or it will output bourne shell code to set COLUMNS and LINES
   7  * appropriately and exit zero.
   8  *
   9  *   $ measure_terminal
  10  *   export COLUMNS=80; export LINES=25;
  11  *
  12  */
  13 
  14 #include <stdio.h>
  15 #include <stdlib.h>
  16 #include <unistd.h>
  17 #include <fcntl.h>
  18 #include <strings.h>
  19 #include <termios.h>
  20 
  21 #define __UNUSED        __attribute__((__unused__))
  22 
  23 #define PREAMBLE        "\x1b[8;"
  24 #define PREAMBLE_LENGTH (sizeof (PREAMBLE) - 1)
  25 
  26 #define REQUEST         "\x1b[18t" "\x1b[0c"
  27 #define REQUEST_LENGTH  (sizeof (REQUEST) - 1)
  28 
  29 #define SHELL_OUTPUT    "export COLUMNS=%d; export LINES=%d;\n"
  30 
  31 static int term_fd = -1;
  32 static struct termios orig_tios;
  33 
  34 static int
  35 reset_mode()
  36 {
  37         if (tcsetattr(term_fd, TCSAFLUSH, &orig_tios) == -1)
  38                 return (-1);
  39 
  40         return (0);
  41 }
  42 
  43 static int
  44 raw_mode()
  45 {
  46         struct termios raw;
  47 
  48         raw = orig_tios;
  49 
  50         /*
  51          * Various raw-mode settings:
  52          */
  53         raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
  54         raw.c_oflag &= ~(OPOST);
  55         raw.c_cflag |= (CS8);
  56         raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
  57 
  58         /*
  59          * Return after 5 seconds pass without data:
  60          */
  61         raw.c_cc[VMIN] = 0;
  62         raw.c_cc[VTIME] = 50;
  63 
  64         if (tcsetattr(term_fd, TCSAFLUSH, &raw) == -1)
  65                 return (-1);
  66 
  67         return (0);
  68 }
  69 
  70 typedef enum cseq_state {
  71         CSEQ_ESCAPE = 1,
  72         CSEQ_BRACKET,
  73         CSEQ_QMARK,
  74         CSEQ_NUMBER
  75 } cseq_state_t;
  76 
  77 static char *
  78 read_cseq()
  79 {
  80         cseq_state_t cs = CSEQ_ESCAPE;
  81         char buf[64];
  82         char *pos = buf;
  83 
  84         while (pos < buf + 64) {
  85                 if (read(term_fd, pos, 1) != 1)
  86                         return (NULL);
  87 
  88                 switch (cs) {
  89                 case CSEQ_ESCAPE:
  90                         if (*pos != '\x1b')
  91                                 return (NULL);
  92                         cs = CSEQ_BRACKET;
  93                         break;
  94                 case CSEQ_BRACKET:
  95                         if (*pos != '[')
  96                                 return (NULL);
  97                         cs = CSEQ_QMARK;
  98                         break;
  99                 case CSEQ_QMARK:
 100                         if ((*pos >= '0' && *pos <= '9') || *pos == '?' ||
 101                             *pos == ';') {
 102                                 cs = CSEQ_NUMBER;
 103                                 break;
 104                         }
 105                         return (NULL);
 106                 case CSEQ_NUMBER:
 107                         if ((*pos >= '0' && *pos <= '9') || *pos == ';')
 108                                 break;
 109                         /*
 110                          * We have the trailing character now, as well.
 111                          */
 112                         pos++;
 113                         *pos = '\0';
 114                         return (strdup(buf));
 115                 default:
 116                         abort();
 117                 }
 118 
 119                 pos++;
 120         }
 121 
 122         return (NULL);
 123 }
 124 
 125 static int
 126 process_size(char *buf)
 127 {
 128         char *rows, *cols, *t;
 129         int rowsi, colsi;
 130 
 131         /*
 132          * Check for the expected preamble in the response from the terminal.
 133          */
 134         if (strlen(buf) <= 4 || strncmp(buf, PREAMBLE, PREAMBLE_LENGTH) != 0)
 135                 return (-1);
 136 
 137         /*
 138          * Split out row and column dimensions:
 139          */
 140         rows = buf + PREAMBLE_LENGTH;
 141         if ((t = strchr(rows, ';')) == NULL)
 142                 return (-1);
 143         *t = '\0';
 144         cols = t + 1;
 145         if ((t = strchr(cols, 't')) == NULL)
 146                 return (-1);
 147         *t = '\0';
 148 
 149         rowsi = atoi(rows);
 150         colsi = atoi(cols);
 151 
 152         if (rowsi <= 1 || colsi <= 1)
 153                 return (-1);
 154 
 155         printf(SHELL_OUTPUT, colsi, rowsi);
 156 
 157         return (0);
 158 }
 159 
 160 int
 161 main(int argc __UNUSED, char **argv __UNUSED)
 162 {
 163         char *buf0 = NULL, *buf1 = NULL;
 164         boolean_t is_match = B_FALSE;
 165         char *term = getenv("TERM");
 166 
 167         /*
 168          * We don't want to run on the VGA console, as it's entirely
 169          * braindead.
 170          */
 171         if (term != NULL && (strcmp(term, "sun") == 0 ||
 172             strcmp(term, "sun-color") == 0))
 173                 return (1);
 174 
 175         /*
 176          * Attempt to open our controlling terminal:
 177          */
 178         if ((term_fd = open("/dev/tty", O_RDWR | O_NOCTTY)) == -1 ||
 179             !isatty(term_fd))
 180                 return (1);
 181 
 182         /*
 183          * Preserve original terminal settings:
 184          */
 185         if (tcgetattr(term_fd, &orig_tios) == -1)
 186                 return (1);
 187 
 188         if (raw_mode() == -1)
 189                 return (1);
 190 
 191         /*
 192          * In order to determine the size of a terminal that behaves like
 193          * an xterm, we can send a control sequence requesting that information:
 194          *
 195          *    ESC [ 18 t
 196          *
 197          * A sufficiently advanced terminal emulator, e.g. xterm or iTerm, will
 198          * respond with a size we can parse:
 199          *
 200          *    ESC [  8 ; ROWS ; COLUMNS t
 201          *
 202          * Unfortunately, not every terminal supports this control sequence
 203          * and most terminals will not generate any data.  It's hard to detect
 204          * the _absence_ of data without using an arbitrary timeout, which may
 205          * be incorrect for some terminals or some high-latency connections,
 206          * so we immediately send a second control sequence which just about
 207          * everything since the VT100 _does_ support.  The response to the
 208          * second escape sequence is easily distinguishable from the first,
 209          * and so we can quickly determine if the terminal supported our query.
 210          */
 211         if (write(term_fd, REQUEST, REQUEST_LENGTH) != REQUEST_LENGTH) {
 212                 reset_mode();
 213                 return (1);
 214         }
 215 
 216         /*
 217          * Read back an entire control sequence:
 218          */
 219         if ((buf0 = read_cseq()) != NULL) {
 220                 /*
 221                  * Check to see if what we read back is the desired response
 222                  * to the _first_ request:
 223                  */
 224                 if (strncmp(buf0, PREAMBLE, PREAMBLE_LENGTH) == 0) {
 225                         /*
 226                          * The terminal understood our first request and responded
 227                          * with a preamble we recognise.  Consume the second
 228                          * escape sequence which we know to be enroute.
 229                          */
 230                         is_match = B_TRUE;
 231                         buf1 = read_cseq();
 232                 }
 233         }
 234 
 235         reset_mode();
 236 
 237         if (!is_match)
 238                 return (1);
 239 
 240         /*
 241          * Process the response:
 242          */
 243         return (process_size(buf0));
 244 }
 245