scroll.c - scroll - scrollbackbuffer program for st
 (HTM) git clone git://git.suckless.org/scroll
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
 (DIR) 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 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 }