diff options
Diffstat (limited to 'files/config/suckless/scroll/scroll.c')
-rw-r--r-- | files/config/suckless/scroll/scroll.c | 594 |
1 files changed, 594 insertions, 0 deletions
diff --git a/files/config/suckless/scroll/scroll.c b/files/config/suckless/scroll/scroll.c new file mode 100644 index 0000000..8f66d54 --- /dev/null +++ b/files/config/suckless/scroll/scroll.c @@ -0,0 +1,594 @@ +/* + * Based on an example code from Roberto E. Vargas Caballero. + * + * See LICENSE file for copyright and license details. + */ + +#include <sys/types.h> +#include <sys/ioctl.h> +#include <sys/wait.h> +#include <sys/queue.h> +#include <sys/resource.h> + +#include <assert.h> +#include <errno.h> +#include <fcntl.h> +#include <poll.h> +#include <pwd.h> +#include <signal.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <termios.h> +#include <unistd.h> + +#if defined(__linux) + #include <pty.h> +#elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) + #include <util.h> +#elif defined(__FreeBSD__) || defined(__DragonFly__) + #include <libutil.h> +#endif + +#define LENGTH(X) (sizeof (X) / sizeof ((X)[0])) + +const char *argv0; + +TAILQ_HEAD(tailhead, line) head; + +struct line { + TAILQ_ENTRY(line) entries; + size_t size; + size_t len; + char *buf; +} *bottom; + +pid_t child; +int mfd; +struct termios dfl; +struct winsize ws; +static bool altscreen = false; /* is alternative screen active */ +static bool doredraw = false; /* redraw upon sigwinch */ + +struct rule { + const char *seq; + enum {SCROLL_UP, SCROLL_DOWN} event; + short lines; +}; + +#include "config.h" + +void +die(const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + + if (fmt[0] && fmt[strlen(fmt)-1] == ':') { + fputc(' ', stderr); + perror(NULL); + } else { + fputc('\n', stderr); + } + + exit(EXIT_FAILURE); +} + +void +sigwinch(int sig) +{ + assert(sig == SIGWINCH); + + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1) + die("ioctl:"); + if (ioctl(mfd, TIOCSWINSZ, &ws) == -1) { + if (errno == EBADF) /* child already exited */ + return; + die("ioctl:"); + } + kill(-child, SIGWINCH); + doredraw = true; +} + +void +reset(void) +{ + if (tcsetattr(STDIN_FILENO, TCSANOW, &dfl) == -1) + die("tcsetattr:"); +} + +/* error avoiding remalloc */ +void * +earealloc(void *ptr, size_t size) +{ + void *mem; + + while ((mem = realloc(ptr, size)) == NULL) { + struct line *line = TAILQ_LAST(&head, tailhead); + + if (line == NULL) + die("realloc:"); + + TAILQ_REMOVE(&head, line, entries); + free(line->buf); + free(line); + } + + return mem; +} + +/* Count string length w/o ansi esc sequences. */ +size_t +strelen(const char *buf, size_t size) +{ + enum {CHAR, BREK, ESC} state = CHAR; + size_t len = 0; + + for (size_t i = 0; i < size; i++) { + char c = buf[i]; + + switch (state) { + case CHAR: + if (c == '\033') + state = BREK; + else + len++; + break; + case BREK: + if (c == '[') { + state = ESC; + } else { + state = CHAR; + len++; + } + break; + case ESC: + if (c >= 64 && c <= 126) + state = CHAR; + break; + } + } + + return len; +} + +/* detect alternative screen switching and clear screen */ +bool +skipesc(char c) +{ + static enum {CHAR, BREK, ESC} state = CHAR; + static char buf[BUFSIZ]; + static size_t i = 0; + + switch (state) { + case CHAR: + if (c == '\033') + state = BREK; + break; + case BREK: + if (c == '[') + state = ESC; + else + state = CHAR; + break; + case ESC: + buf[i++] = c; + if (i == sizeof buf) { + /* TODO: find a better way to handle this situation */ + state = CHAR; + i = 0; + } else if (c >= 64 && c <= 126) { + state = CHAR; + buf[i] = '\0'; + i = 0; + + /* esc seq. enable alternative screen */ + if (strcmp(buf, "?1049h") == 0 || + strcmp(buf, "?1047h") == 0 || + strcmp(buf, "?47h" ) == 0) + altscreen = true; + + /* esc seq. disable alternative screen */ + if (strcmp(buf, "?1049l") == 0 || + strcmp(buf, "?1047l") == 0 || + strcmp(buf, "?47l" ) == 0) + altscreen = false; + + /* don't save cursor move or clear screen */ + /* esc sequences to log */ + switch (c) { + case 'A': + case 'B': + case 'C': + case 'D': + case 'H': + case 'J': + case 'K': + case 'f': + return true; + } + } + break; + } + + return altscreen; +} + +void +getcursorposition(int *x, int *y) +{ + char input[BUFSIZ]; + ssize_t n; + + if (write(STDOUT_FILENO, "\033[6n", 4) == -1) + die("requesting cursor position"); + + do { + if ((n = read(STDIN_FILENO, input, sizeof(input)-1)) == -1) + die("reading cursor position"); + input[n] = '\0'; + } while (sscanf(input, "\033[%d;%dR", y, x) != 2); + + if (*x <= 0 || *y <= 0) + die("invalid cursor position: x=%d y=%d", *x, *y); +} + +void +addline(char *buf, size_t size) +{ + struct line *line = earealloc(NULL, sizeof *line); + + line->size = size; + line->len = strelen(buf, size); + line->buf = earealloc(NULL, size); + memcpy(line->buf, buf, size); + + TAILQ_INSERT_HEAD(&head, line, entries); +} + +void +redraw() +{ + int rows = 0, x, y; + + if (bottom == NULL) + return; + + getcursorposition(&x, &y); + + if (y < ws.ws_row-1) + y--; + + /* wind back bottom pointer by shown history */ + for (; bottom != NULL && TAILQ_NEXT(bottom, entries) != NULL && + rows < y - 1; rows++) + bottom = TAILQ_NEXT(bottom, entries); + + /* clear screen */ + dprintf(STDOUT_FILENO, "\033[2J"); + /* set cursor position to upper left corner */ + write(STDOUT_FILENO, "\033[0;0H", 6); + + /* remove newline of first line as we are at 0,0 already */ + if (bottom->size > 0 && bottom->buf[0] == '\n') + write(STDOUT_FILENO, bottom->buf + 1, bottom->size - 1); + else + write(STDOUT_FILENO, bottom->buf, bottom->size); + + for (rows = ws.ws_row; rows > 0 && + TAILQ_PREV(bottom, tailhead, entries) != NULL; rows--) { + bottom = TAILQ_PREV(bottom, tailhead, entries); + write(STDOUT_FILENO, bottom->buf, bottom->size); + } + + if (bottom == TAILQ_FIRST(&head)) { + /* add new line in front of the shell prompt */ + write(STDOUT_FILENO, "\n", 1); + write(STDOUT_FILENO, "\033[?25h", 6); /* show cursor */ + } else + bottom = TAILQ_NEXT(bottom, entries); +} + +void +scrollup(int n) +{ + int rows = 2, x, y, extra = 0; + struct line *scrollend = bottom; + + if (bottom == NULL) + return; + + getcursorposition(&x, &y); + + if (n < 0) /* scroll by fraction of ws.ws_row, but at least one line */ + n = ws.ws_row > (-n) ? ws.ws_row / (-n) : 1; + + /* wind back scrollend pointer by the current screen */ + while (rows < y && TAILQ_NEXT(scrollend, entries) != NULL) { + scrollend = TAILQ_NEXT(scrollend, entries); + rows += (scrollend->len - 1) / ws.ws_col + 1; + } + + if (rows <= 0) + return; + + /* wind back scrollend pointer n lines */ + for (rows = 0; rows + extra < n && + TAILQ_NEXT(scrollend, entries) != NULL; rows++) { + scrollend = TAILQ_NEXT(scrollend, entries); + extra += (scrollend->len - 1) / ws.ws_col; + } + + /* move the text in terminal rows lines down */ + dprintf(STDOUT_FILENO, "\033[%dT", n); + /* set cursor position to upper left corner */ + write(STDOUT_FILENO, "\033[0;0H", 6); + /* hide cursor */ + write(STDOUT_FILENO, "\033[?25l", 6); + + /* remove newline of first line as we are at 0,0 already */ + if (scrollend->size > 0 && scrollend->buf[0] == '\n') + write(STDOUT_FILENO, scrollend->buf + 1, scrollend->size - 1); + else + write(STDOUT_FILENO, scrollend->buf, scrollend->size); + if (y + n >= ws.ws_row) + bottom = TAILQ_NEXT(bottom, entries); + + /* print rows lines and move bottom forward to the new screen bottom */ + for (; rows > 1; rows--) { + scrollend = TAILQ_PREV(scrollend, tailhead, entries); + if (y + n >= ws.ws_row) + bottom = TAILQ_NEXT(bottom, entries); + write(STDOUT_FILENO, scrollend->buf, scrollend->size); + } + /* move cursor from line n to the old bottom position */ + if (y + n < ws.ws_row) { + dprintf(STDOUT_FILENO, "\033[%d;%dH", y + n, x); + write(STDOUT_FILENO, "\033[?25h", 6); /* show cursor */ + } else + dprintf(STDOUT_FILENO, "\033[%d;0H", ws.ws_row); +} + +void +scrolldown(char *buf, size_t size, int n) +{ + if (bottom == NULL || bottom == TAILQ_FIRST(&head)) + return; + + if (n < 0) /* scroll by fraction of ws.ws_row, but at least one line */ + n = ws.ws_row > (-n) ? ws.ws_row / (-n) : 1; + + bottom = TAILQ_PREV(bottom, tailhead, entries); + /* print n lines */ + while (n > 0 && bottom != NULL && bottom != TAILQ_FIRST(&head)) { + bottom = TAILQ_PREV(bottom, tailhead, entries); + write(STDOUT_FILENO, bottom->buf, bottom->size); + n -= (bottom->len - 1) / ws.ws_col + 1; + } + if (n > 0 && bottom == TAILQ_FIRST(&head)) { + write(STDOUT_FILENO, "\033[?25h", 6); /* show cursor */ + write(STDOUT_FILENO, buf, size); + } else if (bottom != NULL) + bottom = TAILQ_NEXT(bottom, entries); +} + +void +jumpdown(char *buf, size_t size) +{ + int rows = ws.ws_row; + + /* wind back by one page starting from the latest line */ + bottom = TAILQ_FIRST(&head); + for (; TAILQ_NEXT(bottom, entries) != NULL && rows > 0; rows--) + bottom = TAILQ_NEXT(bottom, entries); + + scrolldown(buf, size, ws.ws_row); +} + +void +usage(void) { + die("usage: %s [-Mvh] [-m mem] [program]", argv0); +} + +int +main(int argc, char *argv[]) +{ + int ch; + struct rlimit rlimit; + + argv0 = argv[0]; + + if (getrlimit(RLIMIT_DATA, &rlimit) == -1) + die("getrlimit"); + + const char *optstring = "Mm:vh"; + while ((ch = getopt(argc, argv, optstring)) != -1) { + switch (ch) { + case 'M': + rlimit.rlim_cur = rlimit.rlim_max; + break; + case 'm': + rlimit.rlim_cur = strtoull(optarg, NULL, 0); + if (errno != 0) + die("strtoull: %s", optarg); + break; + case 'v': + die("%s " VERSION, argv0); + break; + case 'h': + default: + usage(); + } + } + argc -= optind; + argv += optind; + + TAILQ_INIT(&head); + + if (isatty(STDIN_FILENO) == 0 || isatty(STDOUT_FILENO) == 0) + die("parent it not a tty"); + + /* save terminal settings for resetting after exit */ + if (tcgetattr(STDIN_FILENO, &dfl) == -1) + die("tcgetattr:"); + if (atexit(reset)) + die("atexit:"); + + /* get window size of the terminal */ + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1) + die("ioctl:"); + + child = forkpty(&mfd, NULL, &dfl, &ws); + if (child == -1) + die("forkpty:"); + if (child == 0) { /* child */ + if (argc >= 1) { + execvp(argv[0], argv); + } else { + struct passwd *passwd = getpwuid(getuid()); + if (passwd == NULL) + die("getpwid:"); + execlp(passwd->pw_shell, passwd->pw_shell, NULL); + } + + perror("execvp"); + _exit(127); + } + + /* set maximum memory size for scrollback buffer */ + if (setrlimit(RLIMIT_DATA, &rlimit) == -1) + die("setrlimit:"); + +#ifdef __OpenBSD__ + if (pledge("stdio tty proc", NULL) == -1) + die("pledge:"); +#endif + + if (signal(SIGWINCH, sigwinch) == SIG_ERR) + die("signal:"); + + struct termios new = dfl; + cfmakeraw(&new); + new.c_cc[VMIN ] = 1; /* return read if at least one byte in buffer */ + new.c_cc[VTIME] = 0; /* no polling time for read from terminal */ + if (tcsetattr(STDIN_FILENO, TCSANOW, &new) == -1) + die("tcsetattr:"); + + size_t size = BUFSIZ, len = 0, pos = 0; + char *buf = calloc(size, sizeof *buf); + if (buf == NULL) + die("calloc:"); + + struct pollfd pfd[2] = { + {STDIN_FILENO, POLLIN, 0}, + {mfd, POLLIN, 0} + }; + + for (;;) { + char input[BUFSIZ]; + + if (poll(pfd, LENGTH(pfd), -1) == -1 && errno != EINTR) + die("poll:"); + + if (doredraw) { + redraw(); + doredraw = false; + } + + if (pfd[0].revents & POLLHUP || pfd[1].revents & POLLHUP) + break; + + if (pfd[0].revents & POLLIN) { + ssize_t n = read(STDIN_FILENO, input, sizeof(input)-1); + + if (n == -1 && errno != EINTR) + die("read:"); + if (n == 0) + break; + + input[n] = '\0'; + + if (altscreen) + goto noevent; + + for (size_t i = 0; i < LENGTH(rules); i++) { + if (strncmp(rules[i].seq, input, + strlen(rules[i].seq)) == 0) { + if (rules[i].event == SCROLL_UP) + scrollup(rules[i].lines); + if (rules[i].event == SCROLL_DOWN) + scrolldown(buf, len, + rules[i].lines); + goto out; + } + } + noevent: + if (write(mfd, input, n) == -1) + die("write:"); + + if (bottom != TAILQ_FIRST(&head)) + jumpdown(buf, len); + } + out: + if (pfd[1].revents & POLLIN) { + ssize_t n = read(mfd, input, sizeof(input)-1); + + if (n == -1 && errno != EINTR) + die("read:"); + if (n == 0) /* on exit of child we continue here */ + continue; /* let signal handler catch SIGCHLD */ + + input[n] = '\0'; + + /* don't print child output while scrolling */ + if (bottom == TAILQ_FIRST(&head)) + if (write(STDOUT_FILENO, input, n) == -1) + die("write:"); + + /* iterate over the input buffer */ + for (char *c = input; n-- > 0; c++) { + /* don't save alternative screen and */ + /* clear screen esc sequences to scrollback */ + if (skipesc(*c)) + continue; + + if (*c == '\n') { + addline(buf, len); + /* only advance bottom if scroll is */ + /* at the end of the scroll back */ + if (bottom == NULL || + TAILQ_PREV(bottom, tailhead, + entries) == TAILQ_FIRST(&head)) + bottom = TAILQ_FIRST(&head); + + memset(buf, 0, size); + len = pos = 0; + buf[pos++] = '\r'; + } else if (*c == '\r') { + pos = 0; + continue; + } + buf[pos++] = *c; + if (pos > len) + len = pos; + if (len == size) { + size *= 2; + buf = earealloc(buf, size); + } + } + } + } + + if (close(mfd) == -1) + die("close:"); + + int status; + if (waitpid(child, &status, 0) == -1) + die("waitpid:"); + + return WEXITSTATUS(status); +} |