#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define ctrl(ch) ((ch) & 0x1f) #define BS 8 #define CR 13 #define ESC 27 #define DEL 127 #ifdef __GNUC__ #define printf_attr(fmtpos, vargpos) __attribute__ ((format (printf, \ fmtpos, vargpos))) #else #define printf_attr(fmtpos, vargpos) #endif enum status_flags { stat_dirty = 1, // display needs refresh stat_eof = 2, // end of data reached stat_susp = 4, // display refresh suspended stat_htmode = 8, // head trigger mode stat_ttmode = 16, // tail trigger mode stat_trgrd = 32, // triggered flag stat_grep = 64, // grep mode stat_force = 128 // force refresh even if clean }; typedef struct grep { char *pat; regex_t rx; int inv; } grep; #define cmdsize 256 #define maxgrep 64 typedef struct dstr { int refs; size_t len; char str[]; } dstr; #define dstr_of(str) ((dstr *) ((str) - sizeof (dstr))) static int poll_interval = 1000; static int long_interval = 10000; static int regex_flags = 0; static char **snapshot; static int snaplines; static char *trigpat; static regex_t trigex; static grep grepstack[maxgrep]; static int ngrep; static char **cmdhist; static unsigned ncmdhist; static char **pathist; static unsigned npathist; volatile sig_atomic_t winch; static void panic(const char *fmt, ...) { va_list vl; va_start (vl, fmt); vfprintf(stderr, fmt, vl); abort(); } printf_attr(1, 2) static void error(const char *fmt, ...) { va_list vl; va_start (vl, fmt); vfprintf(stderr, fmt, vl); exit(EXIT_FAILURE); } static char *dsref(char *str) { dstr *ds = dstr_of(str); ds->refs++; return str; } static void dsdrop(char *str) { if (str) { dstr *ds = dstr_of(str); assert (ds->refs > 0); if (!--ds->refs) free(ds); } } static size_t dslen(const char *str) { const dstr *ds = dstr_of(str); return ds->len; } static char *dsgrow(char *str, size_t len) { dstr *ds = str ? dstr_of(str) : 0; size_t size = sizeof *ds + len + 1; assert (ds == 0 || ds->refs == 1); if (size < len) panic("string size overflow"); ds = realloc(ds, size); if (ds == 0) panic("out of memory"); ds->refs = 1; ds->len = len; ds->str[len] = 0; return ds->str; } static char *dsdup(char *str) { size_t len = strlen(str); char *copy = dsgrow(0, len); memcpy(copy, str, len); return copy; } printf_attr(1, 2) static char *dsdupf(char *fmt, ...) { size_t len = 256, needed; char *out = dsgrow(0, len); for (;;) { va_list vl; va_start (vl, fmt); needed = vsnprintf(out, len + 1, fmt, vl); va_end (vl); if (needed <= len) break; len = needed; } return dsgrow(out, needed); } static char *addch(char *line, int ch) { size_t len = line ? dslen(line) : 0; if (len + 1 > len) { char *nline = dsgrow(line, len + 1); if (nline == 0) panic("out of memory"); nline[len] = ch; return nline; } panic("line overflow"); abort(); } static char *addchesc(char *line, int ch) { if (ch == DEL) { line = addch(line, '^'); line = addch(line, '?'); } else if (ch < 32) { line = addch(line, '^'); line = addch(line, ch + 64); } else { line = addch(line, ch); } return line; } static char *getln(FILE *stream) { char *line = 0; for (;;) { int ch = getc(stream); if (ch == EOF) return line; if (ch == '\n') { if (line) return line; return addch(line, 0); } line = addchesc(line, ch); } } static void usage(const char *name) { fprintf(stderr, "\nUsage: %s [options]\n\n" "-i realnum poll interval (s)\n" "-l realnum long update interval (s)\n" "-n integer display size (# of lines)\n" "-d do not quit on end-of-input\n\n" "For a full description, see the manual page.\n\n", name); exit(EXIT_FAILURE); } static void clrline() { printf("\r\033[J"); } static void clreol(int nl) { printf("\033[K"); if (nl) putchar('\n'); } static void drawline(const char *line, int hpos, int columns) { size_t len = dslen(line); if ((size_t) hpos <= len) { if (hpos) { line += hpos; len -= hpos; putchar('>'); columns--; } if (len < (size_t) columns) { fputs(line, stdout); clreol(1); } else { for (int i = 0; i < columns - 1; i++) putchar(line[i]); puts("<"); } } else { putchar('>'); clreol(1); } } static void drawstatus(int columns, unsigned stat, char *cmd) { char status[cmdsize] = "", *ptr = status; size_t lim = sizeof status; if (columns - 1 < (int) lim) lim = columns - 1; char *end = ptr + lim; if (cmd) { snprintf(status, lim, "%s", cmd); } else if ((stat & (stat_eof | stat_susp | stat_htmode | stat_ttmode | stat_grep))) { if ((stat & stat_eof)) ptr += snprintf(ptr, end - ptr, "EOF "); if ((stat & stat_grep)) { ptr += snprintf(ptr, end - ptr, "GREP ("); for (int i = 0; i < ngrep; i++) { grep *gr = &grepstack[i]; ptr += snprintf(ptr, end - ptr, "%s%s%c ", gr->inv ? "!" : "", gr->pat, (i < ngrep - 1) ? ',' : ')'); } } if ((stat & stat_htmode)) ptr += snprintf(ptr, end - ptr, "TRIG (/%s) ", trigpat); else if ((stat & stat_ttmode)) ptr += snprintf(ptr, end - ptr, "TRIG (?%s) ", trigpat); if ((stat & stat_susp)) ptr += snprintf(ptr, end - ptr, "SUSPENDED "); } fputs(status, stdout); clreol(0); fflush(stdout); } static unsigned redraw(char **circbuf, int nlines, int hpos, int columns, unsigned stat, char *cmd) { if ((stat & stat_susp) == 0 && (stat & (stat_htmode | stat_trgrd)) != stat_htmode && (stat & (stat_ttmode | stat_trgrd)) != stat_ttmode) { if (snapshot) { for (int i = 0; i < snaplines; i++) dsdrop(snapshot[i]); } snaplines = nlines; printf("\r\033[%dA", nlines); for (int i = 0; i < nlines; i++) { drawline(circbuf[i], hpos, columns); snapshot[i] = dsref(circbuf[i]); } stat &= ~(stat_dirty | stat_trgrd); } else if ((stat & stat_force)) { printf("\r\033[%dA", snaplines); for (int i = 0; i < snaplines; i++) drawline(snapshot[i], hpos, columns); stat &= ~stat_force; } else { clrline(); } drawstatus(columns, stat, cmd); return stat; } static int getzp(const char *str, char **err) { char *endp; long val = strtol(str, &endp, 10); if (endp == str) { *err = dsdup("number expected"); return -1; } if (val <= 0) { *err = dsdup("positive value required"); return -1; } if (val >= INT_MAX) { *err = dsdup("unreasonably large value"); return -1; } return val; } static int getms(const char *str, char **err) { errno = 0; char *endp; double sec = strtod(str, &endp); if (endp == str) { *err = dsdup("number expected"); return -1; } if ((sec == 0 || sec == HUGE_VAL) && errno != 0) { *err = dsdup("unreasonable real value"); return -1; } if (sec < 0) { *err = dsdup("positive value required"); return -1; } double msec = sec * 1000; if (msec > (double) INT_MAX) { *err = dsdupf("maximum interval is %f", INT_MAX / 1000.0); return -1; } return msec; } static void execute(char *cmd, unsigned *pstat) { char *arg = cmd + 2 + strspn(cmd + 2, " \t"); clrline(); cmd[0] = 0; switch (cmd[1]) { case 'w': case 'a': if (arg[0] == 0) { sprintf(cmd, "file name required!"); break; } else { FILE *f = fopen(arg, cmd[1] == 'w' ? "w" : "a"); int ok = 1; if (!f) { sprintf(cmd, "unable to open file"); break; } for (int i = 0; ok && i < snaplines; i++) if (fprintf(f, "%s\n", snapshot[i]) < 0) { sprintf(cmd, "write error!"); ok = 0; break; } fclose(f); if (ok) sprintf(cmd, "saved!"); } break; case '!': if (arg[0] == 0) { sprintf(cmd, "command required!"); break; } else { FILE *p = popen(arg, "w"); int ok = 1; if (!p) { sprintf(cmd, "unable to open command"); fflush(stdout); break; } for (int i = 0; i < snaplines && ok; i++) if (fprintf(p, "%s\n", snapshot[i]) < 0) { sprintf(cmd, "write error!"); ok = 0; break; } pclose(p); if (ok) sprintf(cmd, "piped!"); } break; case 'g': case 'v': { int err; grep *gr = &grepstack[ngrep]; if (arg[0] == 0) { sprintf(cmd, "pattern required!"); break; } if (ngrep >= maxgrep) { sprintf(cmd, "too many greps"); break; } memset(gr, 0, sizeof *gr); if ((err = regcomp(&gr->rx, arg, regex_flags | REG_NOSUB)) != 0) { regerror(err, &gr->rx, cmd, cmdsize); break; } gr->pat = dsdup(arg); if (cmd[1] == 'v') gr->inv = 1; if (ngrep++ == 0) *pstat |= stat_grep; } break; case 'r': while (ngrep > 0) { grep *gr = &grepstack[--ngrep]; regfree(&gr->rx); dsdrop(gr->pat); if (cmd[2] != '!') break; } if (ngrep == 0) *pstat &= ~stat_grep; break; case 'i': case 'l': { char *err = 0; int interval = getms(arg, &err); if (interval < 0) snprintf(cmd, cmdsize, "%s", err); else if (cmd[1] == 'i') poll_interval = interval; else long_interval = interval; dsdrop(err); } break; case 'E': regex_flags = REG_EXTENDED; break; case 'B': regex_flags = 0; break; default: sprintf(cmd, "bad command"); break; } fflush(stdout); } static void ttyset(int fd, struct termios *tty) { if (tcsetattr(fd, TCSANOW, tty) < 0) panic("unable to set TTY parameters"); } static void ttyget(int fd, struct termios *tty) { if (tcgetattr(fd, tty) < 0) panic("unable to get TTY parameters"); } static void sigwinch(int sig) { (void) sig; winch = 1; } static char **resizebuf(char **buf, size_t nlfrom, size_t nlto) { if ((buf = realloc(buf, sizeof *buf * nlto)) == 0) panic("out of memory"); if (nlfrom < nlto) memset(buf + nlfrom, 0, (nlto - nlfrom) * sizeof *buf); return buf; } int main(int argc, char **argv) { char *line = 0; FILE *tty = fopen("/dev/tty", "r+"); int maxlines = 15, nlines = 0, maxed = 0; int opt; int fd = fileno(stdin); int ttyfd = tty ? fileno(tty) : -1; char **circbuf; struct termios tty_saved, tty_new; struct winsize ws = { 0 }; int columns = 80; enum kbd_state { kbd_cmd, kbd_esc, kbd_bkt, kbd_exit, kbd_colon, kbd_result, kbd_trig }; int auto_quit = 1; int exit_status = EXIT_FAILURE; char cmdbuf[cmdsize], *curcmd = 0, *savedcmd = 0; #ifdef SIGWINCH static struct sigaction sa; #endif if (fd < 0) panic("unable to obtain input file descriptor"); if (ttyfd < 0) panic("unable to open /dev/tty"); while ((opt = getopt(argc, argv, "n:i:l:dEB")) != -1) { switch (opt) { case 'n': { char *err; if ((maxlines = getzp(optarg, &err)) < 0) { error("-%c option: %s\n", opt, err); return EXIT_FAILURE; } } break; case 'i': case 'l': { char *err; int interval = getms(optarg, &err); if (interval < 0) { error("-%c option: %s\n", opt, err); return EXIT_FAILURE; } if (opt == 'i') poll_interval = interval; else long_interval = interval; } break; case 'd': auto_quit = 0; break; case 'E': regex_flags = REG_EXTENDED; break; case 'B': regex_flags = 0; break; default: usage(argv[0]); } } if (maxlines <= 0 || maxlines > 1000) error("%d is an unreasonable number of lines to display\n", maxlines); if ((circbuf = calloc(sizeof *circbuf, maxlines)) == 0) panic("out of memory"); if ((snapshot = calloc(sizeof *snapshot, maxlines)) == 0) panic("out of memory"); if (isatty(fd)) { while ((line = getln(stdin))) { puts(line); dsdrop(line); } return 0; } if (ioctl(ttyfd, TIOCGWINSZ, &ws) == 0 && ws.ws_row != 0) { if (maxlines >= ws.ws_row) { maxlines = ws.ws_row - 1; maxed = 1; } columns = ws.ws_col; } ttyget(ttyfd, &tty_saved); tty_new = tty_saved; tty_new.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL); tty_new.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); tty_new.c_cc[VMIN] = 1; tty_new.c_cc[VTIME] = 0; ttyset(ttyfd, &tty_new); setvbuf(tty, NULL, _IONBF, 0); if (fcntl(fd, F_SETFL, O_NONBLOCK) < 0) panic("unable to set stdin nonblocking"); #ifdef SIGWINCH sa.sa_handler = sigwinch; sigaction(SIGWINCH, &sa, NULL); #endif for (unsigned stat = stat_dirty, hpos = 0, kbd_state = kbd_cmd, kbd_prev = kbd_cmd, lasttime = ~0U, workbout = 1024, work = workbout, histpos = 0; kbd_state != kbd_exit ;) { int force = 0, nfds = 2, pollms = poll_interval; struct pollfd pe[2] = { { .fd = ttyfd, .events = POLLIN | POLLHUP | POLLERR }, { .fd = fd, .events = POLLIN | POLLHUP | POLLERR }, }; if ((stat & stat_eof) == 0) { int ch; while ((ch = getc(stdin)) != EOF && ch != '\n') line = addchesc(line, ch); if (ch == EOF) { if (feof(stdin) || (errno != EAGAIN && errno != EWOULDBLOCK)) { nfds = 1; stat |= stat_eof; stat = redraw(circbuf, nlines, hpos, columns, stat, curcmd); if (!ferror(stdin)) exit_status = 0; if (auto_quit) { clrline(); break; } } clearerr(stdin); } else { nfds = 1; line = addch(line, 0); if ((stat & stat_grep)) { int i; for (i = 0; i < ngrep; i++) { grep *gr = &grepstack[i]; int match = regexec(&gr->rx, line, 0, NULL, 0) == 0; if (match == gr->inv) break; } if (i < ngrep) { dsdrop(line); line = 0; } } if (line) { if ((stat & stat_htmode)) if (regexec(&trigex, line, 0, NULL, 0) == 0) stat |= stat_trgrd; if (nlines == maxlines) { dsdrop(circbuf[0]); memmove(circbuf, circbuf + 1, (nlines - 1) * sizeof *circbuf); circbuf[nlines - 1] = line; stat |= stat_dirty; if ((stat & stat_ttmode)) if (regexec(&trigex, circbuf[0], 0, NULL, 0) == 0) stat |= stat_trgrd; } else { circbuf[nlines++] = line; if ((stat & stat_susp) == 0) { snapshot[snaplines++] = dsref(line); clrline(); drawline(line, hpos, columns); drawstatus(columns, stat, curcmd); } } line = 0; } } } else { nfds = 1; } if (winch) { winch = 0; if (ioctl(ttyfd, TIOCGWINSZ, &ws) == 0) { if (maxed) { circbuf = resizebuf(circbuf, maxlines, ws.ws_row - 1); snapshot = resizebuf(snapshot, maxlines, ws.ws_row - 1); maxlines = ws.ws_row - 1; } else { if (maxlines >= ws.ws_row) { maxlines = ws.ws_row - 1; maxed = 1; } } if (nlines > maxlines) nlines = maxlines; if (snaplines > maxlines) snaplines = maxlines; columns = ws.ws_col; } stat |= stat_force; force = 1; } if ((stat & stat_eof)) pollms = -1; else if (nfds < 2) pollms = 0; if ((stat & (stat_trgrd | stat_susp)) == stat_trgrd) force = 1; if (pollms == 0 && !force && work-- > 0) continue; work = workbout; if (!force) { struct timeval tv; unsigned now; gettimeofday(&tv, NULL); now = (((unsigned) tv.tv_sec)%1000000)*1000 + tv.tv_usec/1000; if (lasttime == ~0U || now - lasttime > (unsigned) long_interval) { if ((stat & stat_dirty) && nlines == maxlines) force = 1; lasttime = now; } } if (force) stat = redraw(circbuf, nlines, hpos, columns, stat, curcmd); if (poll(pe, nfds, pollms) <= 0) { if (pollms) { if ((stat & stat_dirty) && nlines == maxlines) stat = redraw(circbuf, nlines, hpos, columns, stat, curcmd); if (kbd_state == kbd_esc || kbd_state == kbd_result) { kbd_state = kbd_cmd; curcmd = 0; clrline(); drawstatus(columns, stat, curcmd); } } work = workbout += workbout / 4; } else { if ((pe[0].revents)) { int ch = getc(tty); if (workbout > 16) work = workbout /= 2; if (ch == ctrl('z')) { ttyset(ttyfd, &tty_saved); kill(0, SIGTSTP); ttyset(ttyfd, &tty_new); for (int i = 0; i < nlines; i++) puts(""); stat = redraw(circbuf, nlines, hpos, columns, stat | stat_force, curcmd); continue; } fakecmd: switch (kbd_state) { case kbd_result: kbd_state = kbd_cmd; stat |= stat_dirty; curcmd = 0; // fallthrough case kbd_cmd: switch (ch) { case 'q': case 3: kbd_state = kbd_exit; break; case 'h': if (hpos >= 8) { hpos -= 8; stat |= stat_force; } break; case 'l': if (hpos < 10000) { hpos += 8; stat |= stat_force; } break; case '0': hpos = 0; stat |= stat_force; break; case ' ': if ((stat & stat_eof) == 0) stat |= stat_susp; break; case CR: stat &= ~stat_susp; break; case ESC: kbd_prev = kbd_state; kbd_state = kbd_esc; break; case ':': kbd_state = kbd_colon; histpos = 0; cmdbuf[0] = ch; cmdbuf[1] = 0; curcmd = cmdbuf; break; case '/': case '?': kbd_state = kbd_trig; histpos = 0; cmdbuf[0] = ch; cmdbuf[1] = 0; curcmd = cmdbuf; break; case '+': if (ws.ws_row && maxlines >= ws.ws_row - 1) break; circbuf = resizebuf(circbuf, maxlines, maxlines + 1); snapshot = resizebuf(snapshot, maxlines, maxlines + 1); maxlines++; if (maxlines == ws.ws_row - 1) maxed = 1; break; } break; case kbd_esc: if (ch == '[') { kbd_state = kbd_bkt; break; } kbd_state = kbd_cmd; curcmd = 0; break; case kbd_bkt: kbd_state = kbd_prev; if (kbd_prev == kbd_cmd) switch (ch) { case 'D': ch = 'h'; goto fakecmd; case 'C': ch = 'l'; goto fakecmd; case 'H': ch = '0'; goto fakecmd; } switch (ch) { case 'A': ch = ctrl('p'); goto fakecmd; case 'B': ch = ctrl('n'); goto fakecmd; } break; case kbd_trig: case kbd_colon: switch (ch) { case ESC: kbd_prev = kbd_state; kbd_state = kbd_esc; break; case CR: case ctrl('c'): if (ch == CR) { if (cmdbuf[1]) { unsigned *pnhist = (kbd_state == kbd_colon ? &ncmdhist : &npathist); unsigned nhist = *pnhist; char ***hist = (kbd_state == kbd_colon ? &cmdhist : &pathist); if (nhist == 0 || strcmp(cmdbuf, (*hist)[0]) != 0) { if ((*hist = realloc(*hist, sizeof **hist * (nhist + 1))) == 0) panic("out of memory"); memmove(*hist + 1, *hist, sizeof **hist * nhist); *pnhist = nhist + 1; (*hist)[0] = dsdup(cmdbuf); } } if (kbd_state == kbd_trig) { if (trigpat) { regfree(&trigex); dsdrop(trigpat); trigpat = 0; } stat &= ~(stat_htmode | stat_ttmode); } if (kbd_state == kbd_colon && cmdbuf[1]) { execute(cmdbuf, &stat); if (cmdbuf[0] != 0) { kbd_state = kbd_result; break; } } else if (cmdbuf[1]) { int err; trigpat = dsdup(cmdbuf + 1); if ((err = regcomp(&trigex, trigpat, regex_flags | REG_NOSUB))) { regerror(err, &trigex, cmdbuf, sizeof cmdbuf); if (columns < (int) sizeof cmdbuf - 1) cmdbuf[columns] = 0; kbd_state = kbd_result; dsdrop(trigpat); trigpat = 0; break; } stat |= (cmdbuf[0] == '/' ? stat_htmode : stat_ttmode); } } kbd_state = kbd_cmd; curcmd = 0; break; case BS: case DEL: { size_t len = strlen(cmdbuf); if (len == 1) { kbd_state = kbd_cmd; curcmd = 0; } else { cmdbuf[--len] = 0; } } break; case ctrl('u'): cmdbuf[1] = 0; break; case ctrl('w'): { size_t len = strlen(cmdbuf); while (len > 1 && isspace((unsigned char) cmdbuf[len - 1])) len--; while (len > 1 && !isspace((unsigned char) cmdbuf[len - 1])) len--; cmdbuf[len] = 0; } break; case ctrl('p'): case ctrl('n'): { unsigned nhist = (kbd_state == kbd_colon ? ncmdhist : npathist); char ***hist = (kbd_state == kbd_colon ? &cmdhist : &pathist); if (ch == ctrl('p')) { if (histpos == 0) { dsdrop(savedcmd); savedcmd = dsdup(cmdbuf); } else { dsdrop((*hist)[histpos-1]); (*hist)[histpos-1] = dsdup(cmdbuf); } if (histpos < nhist) { char *cmd = (*hist)[histpos++]; strcpy(cmdbuf, cmd); } } else { if (histpos >= 1) { dsdrop((*hist)[histpos-1]); (*hist)[histpos-1] = dsdup(cmdbuf); } if (histpos > 1) { char *cmd = (*hist)[--histpos - 1]; strcpy(cmdbuf, cmd); } else if (histpos == 1) { --histpos; strcpy(cmdbuf, savedcmd); dsdrop(savedcmd); savedcmd = 0; } } } break; default: if (isprint(ch)) { size_t len = strlen(cmdbuf); if (len < sizeof cmdbuf - 1 && (int) len < columns - 1) { cmdbuf[len++] = ch; cmdbuf[len] = 0; } } break; } break; case kbd_exit: break; } if ((stat & stat_dirty)) { stat = redraw(circbuf, nlines, hpos, columns, stat, curcmd); } else switch (kbd_state) { case kbd_colon: case kbd_trig: case kbd_result: case kbd_cmd: clrline(); drawstatus(columns, stat, curcmd); } } else { work = workbout += workbout / 4; } } } clrline(); ttyset(ttyfd, &tty_saved); return exit_status; }