594 lines
12 KiB
C
594 lines
12 KiB
C
/*
|
|
* 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);
|
|
}
|