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 situation */ 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)) == -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 cursor */ 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 line */ 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 - 1); 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 bottom */ 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 cursor */ 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 line */ 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 cursor */ 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 in buffer */ 477 new.c_cc[VTIME] = 0; /* no polling time for read from terminal */ 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(input)-1); 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_DOWN) 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 continue here */ 543 continue; /* let signal handler catch SIGCHLD */ 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 scrollback */ 556 if (skipesc(*c)) 557 continue; 558 559 if (*c == '\n') { 560 addline(buf, len); 561 /* only advance bottom if scroll is */ 562 /* at the end of the scroll back */ 563 if (bottom == NULL || 564 TAILQ_PREV(bottom, tailhead, 565 entries) == TAILQ_FIRST(&head)) 566 bottom = TAILQ_FIRST(&head); 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 }