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 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 }