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