#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 }; typedef struct dstr { int refs; size_t len; char str[]; } dstr; #define dstr_of(str) ((dstr *) ((str) - sizeof (dstr))) char **snapshot; int snaplines; char *trigpat; regex_t trigex; static void panic(const char *fmt, ...) { va_list vl; va_start (vl, fmt); vfprintf(stderr, fmt, vl); abort(); } 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; } 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 == 127) { 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 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('>'); } if (len < (size_t) columns - 1) { puts(line); } else { for (int i = 0; i < columns - 2; i++) putchar(line[i]); puts("<"); } } else { puts(">"); } } static void clrline() { printf("\r\033[J"); } static void drawstatus(unsigned stat, char *cmd) { if (cmd) { printf("%s", cmd); } else if ((stat & (stat_eof | stat_susp | stat_htmode | stat_ttmode))) { if ((stat & stat_eof)) printf("EOF "); else if ((stat & stat_htmode)) printf("TRIG (/%s) ", trigpat); else if ((stat & stat_ttmode)) printf("TRIG (?%s) ", trigpat); if ((stat & stat_susp)) printf("SUSPENDED "); } else { clrline(); } fflush(stdout); } static void 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\033[J", nlines); for (int i = 0; i < nlines; i++) { drawline(circbuf[i], hpos, columns); snapshot[i] = dsref(circbuf[i]); } } else { printf("\r\033[%dA\033[J", snaplines); for (int i = 0; i < snaplines; i++) drawline(snapshot[i], hpos, columns); } drawstatus(stat, cmd); } static void execute(char *cmd) { 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; default: sprintf(cmd, "bad command"); break; } fflush(stdout); } int main(int argc, char **argv) { char *line = 0; FILE *tty = fopen("/dev/tty", "r+"); int maxlines = 15, nlines = 0; int poll_interval = 1000; int long_interval = 10000; 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_htrig, kbd_ttrig }; int auto_quit = 1; int exit_status = EXIT_FAILURE; char cmdbuf[100], *curcmd = 0; 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:d")) != -1) { switch (opt) { case 'n': maxlines = atoi(optarg); break; case 'i': case 'l': { double interval = atof(optarg) * 1000; if (interval > (double) INT_MAX) error("maximum interval is %f\n", INT_MAX / 1000.0); if (opt == 'i') poll_interval = interval; else long_interval = interval; } break; case 'd': auto_quit = 0; break; default: usage(argv[0]); } } if (maxlines <= 0 || maxlines > 1000) error("%d is an unreasonable number of lines to display", 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; columns = ws.ws_col; } if (tcgetattr(ttyfd, &tty_saved) < 0) panic("unable to get TTY parameters"); 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; if (tcsetattr(ttyfd, TCSANOW, &tty_new) < 0) panic("unable to set TTY parameters"); setvbuf(tty, NULL, _IONBF, 0); if (fcntl(fd, F_SETFL, O_NONBLOCK) < 0) panic("unable to set stdin nonblocking"); for (unsigned stat = stat_dirty, hpos = 0, kbd_state = kbd_cmd, lasttime = ~0U, work = 1000; 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; redraw(circbuf, nlines, hpos, columns, stat, curcmd); stat &= ~stat_dirty; if (!ferror(stdin)) exit_status = 0; if (auto_quit) { clrline(); break; } } clearerr(stdin); } else { nfds = 1; line = addch(line, 0); 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(stat, curcmd); } } line = 0; } } else { nfds = 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 = 1000; 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; stat &= ~stat_dirty; } } if (force) { redraw(circbuf, nlines, hpos, columns, stat, curcmd); stat &= ~(stat_dirty | stat_trgrd); } if (poll(pe, nfds, pollms) <= 0) { if (pollms) { if ((stat & stat_dirty) && nlines == maxlines) { redraw(circbuf, nlines, hpos, columns, stat, curcmd); stat &= ~stat_dirty; } if (kbd_state == kbd_esc || kbd_state == kbd_result) { kbd_state = kbd_cmd; curcmd = 0; clrline(); drawstatus(stat, curcmd); } } } else { if ((pe[0].revents)) { int ch = getc(tty); 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; clrline(); fflush(stdout); break; case 'h': if (hpos >= 8) { hpos -= 8; stat |= stat_dirty; } break; case 'l': if (hpos < 10000) { hpos += 8; stat |= stat_dirty; } break; case '0': hpos = 0; stat |= stat_dirty; break; case ' ': if ((stat & stat_eof) == 0) stat |= (stat_dirty | stat_susp); break; case 13: stat &= ~stat_susp; stat |= stat_dirty; break; case 27: kbd_state = kbd_esc; break; case ':': kbd_state = kbd_colon; cmdbuf[0] = ch; cmdbuf[1] = 0; curcmd = cmdbuf; break; case '/': kbd_state = kbd_htrig; cmdbuf[0] = ch; cmdbuf[1] = 0; curcmd = cmdbuf; break; case '?': kbd_state = kbd_ttrig; cmdbuf[0] = ch; cmdbuf[1] = 0; curcmd = cmdbuf; break; } break; case kbd_esc: kbd_state = kbd_cmd; if (ch == '[') kbd_state = kbd_bkt; break; case kbd_bkt: kbd_state = kbd_cmd; switch (ch) { case 'D': ch = 'h'; goto fakecmd; case 'C': ch = 'l'; goto fakecmd; case 'H': ch = '0'; goto fakecmd; } break; case kbd_htrig: case kbd_ttrig: if (trigpat) { regfree(&trigex); dsdrop(trigpat); trigpat = 0; } stat &= ~(stat_htmode | stat_ttmode); // fallthrough case kbd_colon: switch (ch) { case 27: case 13: case 3: stat |= stat_dirty; if (ch == 13) { if (kbd_state == kbd_colon && cmdbuf[1]) { execute(cmdbuf); if (cmdbuf[0] != 0) { kbd_state = kbd_result; break; } } else if (cmdbuf[1]) { int err; trigpat = dsdup(cmdbuf + 1); if ((err = regcomp(&trigex, trigpat, REG_EXTENDED | 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 |= (kbd_state == kbd_htrig ? stat_htmode : stat_ttmode); } } kbd_state = kbd_cmd; curcmd = 0; break; case 8: case 127: { size_t len = strlen(cmdbuf); if (len == 1) { kbd_state = kbd_cmd; curcmd = 0; stat |= stat_dirty; } else { cmdbuf[--len] = 0; } } break; case 21: cmdbuf[1] = 0; break; case 23: { 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; 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)) { redraw(circbuf, nlines, hpos, columns, stat, curcmd); stat &= ~stat_dirty; } else switch (kbd_state) { case kbd_colon: case kbd_htrig: case kbd_ttrig: case kbd_result: clrline(); drawstatus(stat, curcmd); } } } } if (tcsetattr(ttyfd, TCSANOW, &tty_saved) < 0) panic("unable to restore TTY parameters"); return exit_status; }