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