scroll.c - scroll - scrollbackbuffer program for st | |
git clone git://git.suckless.org/scroll | |
Log | |
Files | |
Refs | |
README | |
LICENSE | |
--- | |
scroll.c (12817B) | |
--- | |
1 /* | |
2 * Based on an example code from Roberto E. Vargas Caballero. | |
3 * | |
4 * See LICENSE file for copyright and license details. | |
5 */ | |
6 | |
7 #include <sys/types.h> | |
8 #include <sys/ioctl.h> | |
9 #include <sys/wait.h> | |
10 #include <sys/queue.h> | |
11 #include <sys/resource.h> | |
12 | |
13 #include <assert.h> | |
14 #include <errno.h> | |
15 #include <fcntl.h> | |
16 #include <poll.h> | |
17 #include <pwd.h> | |
18 #include <signal.h> | |
19 #include <stdarg.h> | |
20 #include <stdbool.h> | |
21 #include <stdio.h> | |
22 #include <stdlib.h> | |
23 #include <string.h> | |
24 #include <termios.h> | |
25 #include <unistd.h> | |
26 | |
27 #if defined(__linux) | |
28 #include <pty.h> | |
29 #elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) | |
30 #include <util.h> | |
31 #elif defined(__FreeBSD__) || defined(__DragonFly__) | |
32 #include <libutil.h> | |
33 #endif | |
34 | |
35 #define LENGTH(X) (sizeof (X) / sizeof ((X)[0])) | |
36 | |
37 const char *argv0; | |
38 | |
39 TAILQ_HEAD(tailhead, line) head; | |
40 | |
41 struct line { | |
42 TAILQ_ENTRY(line) entries; | |
43 size_t size; | |
44 size_t len; | |
45 char *buf; | |
46 } *bottom; | |
47 | |
48 pid_t child; | |
49 int mfd; | |
50 struct termios dfl; | |
51 struct winsize ws; | |
52 static bool altscreen = false; /* is alternative screen active */ | |
53 static bool doredraw = false; /* redraw upon sigwinch */ | |
54 | |
55 struct rule { | |
56 const char *seq; | |
57 enum {SCROLL_UP, SCROLL_DOWN} event; | |
58 short lines; | |
59 }; | |
60 | |
61 #include "config.h" | |
62 | |
63 void | |
64 die(const char *fmt, ...) | |
65 { | |
66 va_list ap; | |
67 va_start(ap, fmt); | |
68 vfprintf(stderr, fmt, ap); | |
69 va_end(ap); | |
70 | |
71 if (fmt[0] && fmt[strlen(fmt)-1] == ':') { | |
72 fputc(' ', stderr); | |
73 perror(NULL); | |
74 } else { | |
75 fputc('\n', stderr); | |
76 } | |
77 | |
78 exit(EXIT_FAILURE); | |
79 } | |
80 | |
81 void | |
82 sigwinch(int sig) | |
83 { | |
84 assert(sig == SIGWINCH); | |
85 | |
86 if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1) | |
87 die("ioctl:"); | |
88 if (ioctl(mfd, TIOCSWINSZ, &ws) == -1) { | |
89 if (errno == EBADF) /* child already exited */ | |
90 return; | |
91 die("ioctl:"); | |
92 } | |
93 kill(-child, SIGWINCH); | |
94 doredraw = true; | |
95 } | |
96 | |
97 void | |
98 reset(void) | |
99 { | |
100 if (tcsetattr(STDIN_FILENO, TCSANOW, &dfl) == -1) | |
101 die("tcsetattr:"); | |
102 } | |
103 | |
104 /* error avoiding remalloc */ | |
105 void * | |
106 earealloc(void *ptr, size_t size) | |
107 { | |
108 void *mem; | |
109 | |
110 while ((mem = realloc(ptr, size)) == NULL) { | |
111 struct line *line = TAILQ_LAST(&head, tailhead); | |
112 | |
113 if (line == NULL) | |
114 die("realloc:"); | |
115 | |
116 TAILQ_REMOVE(&head, line, entries); | |
117 free(line->buf); | |
118 free(line); | |
119 } | |
120 | |
121 return mem; | |
122 } | |
123 | |
124 /* Count string length w/o ansi esc sequences. */ | |
125 size_t | |
126 strelen(const char *buf, size_t size) | |
127 { | |
128 enum {CHAR, BREK, ESC} state = CHAR; | |
129 size_t len = 0; | |
130 | |
131 for (size_t i = 0; i < size; i++) { | |
132 char c = buf[i]; | |
133 | |
134 switch (state) { | |
135 case CHAR: | |
136 if (c == '\033') | |
137 state = BREK; | |
138 else | |
139 len++; | |
140 break; | |
141 case BREK: | |
142 if (c == '[') { | |
143 state = ESC; | |
144 } else { | |
145 state = CHAR; | |
146 len++; | |
147 } | |
148 break; | |
149 case ESC: | |
150 if (c >= 64 && c <= 126) | |
151 state = CHAR; | |
152 break; | |
153 } | |
154 } | |
155 | |
156 return len; | |
157 } | |
158 | |
159 /* detect alternative screen switching and clear screen */ | |
160 bool | |
161 skipesc(char c) | |
162 { | |
163 static enum {CHAR, BREK, ESC} state = CHAR; | |
164 static char buf[BUFSIZ]; | |
165 static size_t i = 0; | |
166 | |
167 switch (state) { | |
168 case CHAR: | |
169 if (c == '\033') | |
170 state = BREK; | |
171 break; | |
172 case BREK: | |
173 if (c == '[') | |
174 state = ESC; | |
175 else | |
176 state = CHAR; | |
177 break; | |
178 case ESC: | |
179 buf[i++] = c; | |
180 if (i == sizeof buf) { | |
181 /* TODO: find a better way to handle this situat… | |
182 state = CHAR; | |
183 i = 0; | |
184 } else if (c >= 64 && c <= 126) { | |
185 state = CHAR; | |
186 buf[i] = '\0'; | |
187 i = 0; | |
188 | |
189 /* esc seq. enable alternative screen */ | |
190 if (strcmp(buf, "?1049h") == 0 || | |
191 strcmp(buf, "?1047h") == 0 || | |
192 strcmp(buf, "?47h" ) == 0) | |
193 altscreen = true; | |
194 | |
195 /* esc seq. disable alternative screen */ | |
196 if (strcmp(buf, "?1049l") == 0 || | |
197 strcmp(buf, "?1047l") == 0 || | |
198 strcmp(buf, "?47l" ) == 0) | |
199 altscreen = false; | |
200 | |
201 /* don't save cursor move or clear screen */ | |
202 /* esc sequences to log */ | |
203 switch (c) { | |
204 case 'A': | |
205 case 'B': | |
206 case 'C': | |
207 case 'D': | |
208 case 'H': | |
209 case 'J': | |
210 case 'K': | |
211 case 'f': | |
212 return true; | |
213 } | |
214 } | |
215 break; | |
216 } | |
217 | |
218 return altscreen; | |
219 } | |
220 | |
221 void | |
222 getcursorposition(int *x, int *y) | |
223 { | |
224 char input[BUFSIZ]; | |
225 ssize_t n; | |
226 | |
227 if (write(STDOUT_FILENO, "\033[6n", 4) == -1) | |
228 die("requesting cursor position"); | |
229 | |
230 do { | |
231 if ((n = read(STDIN_FILENO, input, sizeof(input)-1)) == … | |
232 die("reading cursor position"); | |
233 input[n] = '\0'; | |
234 } while (sscanf(input, "\033[%d;%dR", y, x) != 2); | |
235 | |
236 if (*x <= 0 || *y <= 0) | |
237 die("invalid cursor position: x=%d y=%d", *x, *y); | |
238 } | |
239 | |
240 void | |
241 addline(char *buf, size_t size) | |
242 { | |
243 struct line *line = earealloc(NULL, sizeof *line); | |
244 | |
245 line->size = size; | |
246 line->len = strelen(buf, size); | |
247 line->buf = earealloc(NULL, size); | |
248 memcpy(line->buf, buf, size); | |
249 | |
250 TAILQ_INSERT_HEAD(&head, line, entries); | |
251 } | |
252 | |
253 void | |
254 redraw() | |
255 { | |
256 int rows = 0, x, y; | |
257 | |
258 if (bottom == NULL) | |
259 return; | |
260 | |
261 getcursorposition(&x, &y); | |
262 | |
263 if (y < ws.ws_row-1) | |
264 y--; | |
265 | |
266 /* wind back bottom pointer by shown history */ | |
267 for (; bottom != NULL && TAILQ_NEXT(bottom, entries) != NULL && | |
268 rows < y - 1; rows++) | |
269 bottom = TAILQ_NEXT(bottom, entries); | |
270 | |
271 /* clear screen */ | |
272 dprintf(STDOUT_FILENO, "\033[2J"); | |
273 /* set cursor position to upper left corner */ | |
274 write(STDOUT_FILENO, "\033[0;0H", 6); | |
275 | |
276 /* remove newline of first line as we are at 0,0 already */ | |
277 if (bottom->size > 0 && bottom->buf[0] == '\n') | |
278 write(STDOUT_FILENO, bottom->buf + 1, bottom->size - 1); | |
279 else | |
280 write(STDOUT_FILENO, bottom->buf, bottom->size); | |
281 | |
282 for (rows = ws.ws_row; rows > 0 && | |
283 TAILQ_PREV(bottom, tailhead, entries) != NULL; rows--) { | |
284 bottom = TAILQ_PREV(bottom, tailhead, entries); | |
285 write(STDOUT_FILENO, bottom->buf, bottom->size); | |
286 } | |
287 | |
288 if (bottom == TAILQ_FIRST(&head)) { | |
289 /* add new line in front of the shell prompt */ | |
290 write(STDOUT_FILENO, "\n", 1); | |
291 write(STDOUT_FILENO, "\033[?25h", 6); /* show cur… | |
292 } else | |
293 bottom = TAILQ_NEXT(bottom, entries); | |
294 } | |
295 | |
296 void | |
297 scrollup(int n) | |
298 { | |
299 int rows = 2, x, y, extra = 0; | |
300 struct line *scrollend = bottom; | |
301 | |
302 if (bottom == NULL) | |
303 return; | |
304 | |
305 getcursorposition(&x, &y); | |
306 | |
307 if (n < 0) /* scroll by fraction of ws.ws_row, but at least one … | |
308 n = ws.ws_row > (-n) ? ws.ws_row / (-n) : 1; | |
309 | |
310 /* wind back scrollend pointer by the current screen */ | |
311 while (rows < y && TAILQ_NEXT(scrollend, entries) != NULL) { | |
312 scrollend = TAILQ_NEXT(scrollend, entries); | |
313 rows += (scrollend->len - 1) / ws.ws_col + 1; | |
314 } | |
315 | |
316 if (rows <= 0) | |
317 return; | |
318 | |
319 /* wind back scrollend pointer n lines */ | |
320 for (rows = 0; rows + extra < n && | |
321 TAILQ_NEXT(scrollend, entries) != NULL; rows++) { | |
322 scrollend = TAILQ_NEXT(scrollend, entries); | |
323 extra += (scrollend->len - 1) / ws.ws_col; | |
324 } | |
325 | |
326 /* move the text in terminal rows lines down */ | |
327 dprintf(STDOUT_FILENO, "\033[%dT", n); | |
328 /* set cursor position to upper left corner */ | |
329 write(STDOUT_FILENO, "\033[0;0H", 6); | |
330 /* hide cursor */ | |
331 write(STDOUT_FILENO, "\033[?25l", 6); | |
332 | |
333 /* remove newline of first line as we are at 0,0 already */ | |
334 if (scrollend->size > 0 && scrollend->buf[0] == '\n') | |
335 write(STDOUT_FILENO, scrollend->buf + 1, scrollend->size… | |
336 else | |
337 write(STDOUT_FILENO, scrollend->buf, scrollend->size); | |
338 if (y + n >= ws.ws_row) | |
339 bottom = TAILQ_NEXT(bottom, entries); | |
340 | |
341 /* print rows lines and move bottom forward to the new screen bo… | |
342 for (; rows > 1; rows--) { | |
343 scrollend = TAILQ_PREV(scrollend, tailhead, entries); | |
344 if (y + n >= ws.ws_row) | |
345 bottom = TAILQ_NEXT(bottom, entries); | |
346 write(STDOUT_FILENO, scrollend->buf, scrollend->size); | |
347 } | |
348 /* move cursor from line n to the old bottom position */ | |
349 if (y + n < ws.ws_row) { | |
350 dprintf(STDOUT_FILENO, "\033[%d;%dH", y + n, x); | |
351 write(STDOUT_FILENO, "\033[?25h", 6); /* show cur… | |
352 } else | |
353 dprintf(STDOUT_FILENO, "\033[%d;0H", ws.ws_row); | |
354 } | |
355 | |
356 void | |
357 scrolldown(char *buf, size_t size, int n) | |
358 { | |
359 if (bottom == NULL || bottom == TAILQ_FIRST(&head)) | |
360 return; | |
361 | |
362 if (n < 0) /* scroll by fraction of ws.ws_row, but at least one … | |
363 n = ws.ws_row > (-n) ? ws.ws_row / (-n) : 1; | |
364 | |
365 bottom = TAILQ_PREV(bottom, tailhead, entries); | |
366 /* print n lines */ | |
367 while (n > 0 && bottom != NULL && bottom != TAILQ_FIRST(&head)) { | |
368 bottom = TAILQ_PREV(bottom, tailhead, entries); | |
369 write(STDOUT_FILENO, bottom->buf, bottom->size); | |
370 n -= (bottom->len - 1) / ws.ws_col + 1; | |
371 } | |
372 if (n > 0 && bottom == TAILQ_FIRST(&head)) { | |
373 write(STDOUT_FILENO, "\033[?25h", 6); /* show cur… | |
374 write(STDOUT_FILENO, buf, size); | |
375 } else if (bottom != NULL) | |
376 bottom = TAILQ_NEXT(bottom, entries); | |
377 } | |
378 | |
379 void | |
380 jumpdown(char *buf, size_t size) | |
381 { | |
382 int rows = ws.ws_row; | |
383 | |
384 /* wind back by one page starting from the latest line */ | |
385 bottom = TAILQ_FIRST(&head); | |
386 for (; TAILQ_NEXT(bottom, entries) != NULL && rows > 0; rows--) | |
387 bottom = TAILQ_NEXT(bottom, entries); | |
388 | |
389 scrolldown(buf, size, ws.ws_row); | |
390 } | |
391 | |
392 void | |
393 usage(void) { | |
394 die("usage: %s [-Mvh] [-m mem] [program]", argv0); | |
395 } | |
396 | |
397 int | |
398 main(int argc, char *argv[]) | |
399 { | |
400 int ch; | |
401 struct rlimit rlimit; | |
402 | |
403 argv0 = argv[0]; | |
404 | |
405 if (getrlimit(RLIMIT_DATA, &rlimit) == -1) | |
406 die("getrlimit"); | |
407 | |
408 const char *optstring = "Mm:vh"; | |
409 while ((ch = getopt(argc, argv, optstring)) != -1) { | |
410 switch (ch) { | |
411 case 'M': | |
412 rlimit.rlim_cur = rlimit.rlim_max; | |
413 break; | |
414 case 'm': | |
415 rlimit.rlim_cur = strtoull(optarg, NULL, 0); | |
416 if (errno != 0) | |
417 die("strtoull: %s", optarg); | |
418 break; | |
419 case 'v': | |
420 die("%s " VERSION, argv0); | |
421 break; | |
422 case 'h': | |
423 default: | |
424 usage(); | |
425 } | |
426 } | |
427 argc -= optind; | |
428 argv += optind; | |
429 | |
430 TAILQ_INIT(&head); | |
431 | |
432 if (isatty(STDIN_FILENO) == 0 || isatty(STDOUT_FILENO) == 0) | |
433 die("parent it not a tty"); | |
434 | |
435 /* save terminal settings for resetting after exit */ | |
436 if (tcgetattr(STDIN_FILENO, &dfl) == -1) | |
437 die("tcgetattr:"); | |
438 if (atexit(reset)) | |
439 die("atexit:"); | |
440 | |
441 /* get window size of the terminal */ | |
442 if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1) | |
443 die("ioctl:"); | |
444 | |
445 child = forkpty(&mfd, NULL, &dfl, &ws); | |
446 if (child == -1) | |
447 die("forkpty:"); | |
448 if (child == 0) { /* child */ | |
449 if (argc >= 1) { | |
450 execvp(argv[0], argv); | |
451 } else { | |
452 struct passwd *passwd = getpwuid(getuid()); | |
453 if (passwd == NULL) | |
454 die("getpwid:"); | |
455 execlp(passwd->pw_shell, passwd->pw_shell, NULL); | |
456 } | |
457 | |
458 perror("execvp"); | |
459 _exit(127); | |
460 } | |
461 | |
462 /* set maximum memory size for scrollback buffer */ | |
463 if (setrlimit(RLIMIT_DATA, &rlimit) == -1) | |
464 die("setrlimit:"); | |
465 | |
466 #ifdef __OpenBSD__ | |
467 if (pledge("stdio tty proc", NULL) == -1) | |
468 die("pledge:"); | |
469 #endif | |
470 | |
471 if (signal(SIGWINCH, sigwinch) == SIG_ERR) | |
472 die("signal:"); | |
473 | |
474 struct termios new = dfl; | |
475 cfmakeraw(&new); | |
476 new.c_cc[VMIN ] = 1; /* return read if at least one byte … | |
477 new.c_cc[VTIME] = 0; /* no polling time for read from ter… | |
478 if (tcsetattr(STDIN_FILENO, TCSANOW, &new) == -1) | |
479 die("tcsetattr:"); | |
480 | |
481 size_t size = BUFSIZ, len = 0, pos = 0; | |
482 char *buf = calloc(size, sizeof *buf); | |
483 if (buf == NULL) | |
484 die("calloc:"); | |
485 | |
486 struct pollfd pfd[2] = { | |
487 {STDIN_FILENO, POLLIN, 0}, | |
488 {mfd, POLLIN, 0} | |
489 }; | |
490 | |
491 for (;;) { | |
492 char input[BUFSIZ]; | |
493 | |
494 if (poll(pfd, LENGTH(pfd), -1) == -1 && errno != EINTR) | |
495 die("poll:"); | |
496 | |
497 if (doredraw) { | |
498 redraw(); | |
499 doredraw = false; | |
500 } | |
501 | |
502 if (pfd[0].revents & POLLHUP || pfd[1].revents & POLLHUP) | |
503 break; | |
504 | |
505 if (pfd[0].revents & POLLIN) { | |
506 ssize_t n = read(STDIN_FILENO, input, sizeof(inp… | |
507 | |
508 if (n == -1 && errno != EINTR) | |
509 die("read:"); | |
510 if (n == 0) | |
511 break; | |
512 | |
513 input[n] = '\0'; | |
514 | |
515 if (altscreen) | |
516 goto noevent; | |
517 | |
518 for (size_t i = 0; i < LENGTH(rules); i++) { | |
519 if (strncmp(rules[i].seq, input, | |
520 strlen(rules[i].seq)) == 0) { | |
521 if (rules[i].event == SCROLL_UP) | |
522 scrollup(rules[i].lines); | |
523 if (rules[i].event == SCROLL_DOW… | |
524 scrolldown(buf, len, | |
525 rules[i].lines); | |
526 goto out; | |
527 } | |
528 } | |
529 noevent: | |
530 if (write(mfd, input, n) == -1) | |
531 die("write:"); | |
532 | |
533 if (bottom != TAILQ_FIRST(&head)) | |
534 jumpdown(buf, len); | |
535 } | |
536 out: | |
537 if (pfd[1].revents & POLLIN) { | |
538 ssize_t n = read(mfd, input, sizeof(input)-1); | |
539 | |
540 if (n == -1 && errno != EINTR) | |
541 die("read:"); | |
542 if (n == 0) /* on exit of child we contin… | |
543 continue; /* let signal handler catch SI… | |
544 | |
545 input[n] = '\0'; | |
546 | |
547 /* don't print child output while scrolling */ | |
548 if (bottom == TAILQ_FIRST(&head)) | |
549 if (write(STDOUT_FILENO, input, n) == -1) | |
550 die("write:"); | |
551 | |
552 /* iterate over the input buffer */ | |
553 for (char *c = input; n-- > 0; c++) { | |
554 /* don't save alternative screen and */ | |
555 /* clear screen esc sequences to scrollb… | |
556 if (skipesc(*c)) | |
557 continue; | |
558 | |
559 if (*c == '\n') { | |
560 addline(buf, len); | |
561 /* only advance bottom if scroll… | |
562 /* at the end of the scroll back… | |
563 if (bottom == NULL || | |
564 TAILQ_PREV(bottom, tailhead, | |
565 entries) == TAILQ_FIRST(&h… | |
566 bottom = TAILQ_FIRST(&he… | |
567 | |
568 memset(buf, 0, size); | |
569 len = pos = 0; | |
570 buf[pos++] = '\r'; | |
571 } else if (*c == '\r') { | |
572 pos = 0; | |
573 continue; | |
574 } | |
575 buf[pos++] = *c; | |
576 if (pos > len) | |
577 len = pos; | |
578 if (len == size) { | |
579 size *= 2; | |
580 buf = earealloc(buf, size); | |
581 } | |
582 } | |
583 } | |
584 } | |
585 | |
586 if (close(mfd) == -1) | |
587 die("close:"); | |
588 | |
589 int status; | |
590 if (waitpid(child, &status, 0) == -1) | |
591 die("waitpid:"); | |
592 | |
593 return WEXITSTATUS(status); | |
594 } |