#include #include #include #include #include #include #include #include #include #include #include #include enum status_flags { stat_dirty = 1, stat_eof = 2, stat_susp = 4 }; 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 *addch(char *line, size_t len, int ch) { if (len + 2 > len) { char *nline = realloc(line, len + 2); if (nline == 0) panic("out of memory"); nline[len] = ch; nline[len + 1] = 0; return nline; } panic("line overflow"); abort(); } static char *getln(FILE *stream) { char *line = 0; size_t len = 0; for (;;) { int ch = getc(stream); if (ch == EOF) return line; if (ch == '\n') { if (line) return line; return addch(line, len, 0); } if (ch == 127) { line = addch(line, len++, '^'); line = addch(line, len++, '?'); } else if (ch < 32) { line = addch(line, len++, '^'); line = addch(line, len++, ch + 64); } else { line = addch(line, len++, 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 = strlen(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 drawstatus(unsigned stat) { if ((stat & (stat_eof | stat_susp))) { if ((stat & stat_eof)) printf("EOF "); if ((stat & stat_susp)) printf("SUSPENDED "); fflush(stdout); } } static void redraw(char **circbuf, int nlines, int hpos, int columns, unsigned stat) { printf("\r\033[%dA\033[J", nlines); for (int i = 0; i < nlines; i++) drawline(circbuf[i], hpos, columns); drawstatus(stat); } static void clear_cur_line() { printf("\r\033[J"); } int main(int argc, char **argv) { char *line; 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 }; int auto_quit = 1; int exit_status = EXIT_FAILURE; if (fd < 0) panic("unable to obtain input file descriptor"); if (ttyfd < 0) panic("unable to open /dev/tty"); while ((opt = getopt(argc, argv, "ni: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 = malloc(sizeof *circbuf * maxlines)) == 0) panic("out of memory"); if (isatty(fd)) { while ((line = getln(stdin))) { puts(line); free(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); for (unsigned stat = stat_dirty, hpos = 0, kbd_state = kbd_cmd, lasttime = ~0U; kbd_state != kbd_exit ;) { struct pollfd pe[2] = { { .fd = ttyfd, .events = POLLIN | POLLHUP | POLLERR }, { .fd = fd, .events = POLLIN | POLLHUP | POLLERR }, }; { 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) redraw(circbuf, nlines, hpos, columns, stat); lasttime = now; stat &= ~stat_dirty; } } if (poll(pe, ((stat & stat_eof)) ? 1 : 2, poll_interval) <= 0) { if ((stat & stat_dirty) && nlines == maxlines) { redraw(circbuf, nlines, hpos, columns, stat); stat &= ~stat_dirty; } kbd_state = kbd_cmd; } else { if ((stat & stat_eof) == 0 && pe[1].revents) { if ((line = getln(stdin))) { if (nlines == maxlines) { if ((stat & stat_susp)) { free(line); } else { free(circbuf[0]); memmove(circbuf, circbuf + 1, (nlines - 1) * sizeof *circbuf); circbuf[nlines - 1] = line; stat |= stat_dirty; } } else { circbuf[nlines++] = line; clear_cur_line(); drawline(line, hpos, columns); drawstatus(stat); } } else { if (auto_quit) kbd_state = kbd_exit; else stat |= stat_eof; redraw(circbuf, nlines, hpos, columns, stat); stat |= stat_eof; stat &= ~stat_dirty; if (!ferror(stdin)) exit_status = 0; continue; } } if ((pe[0].revents)) { int ch = getc(tty); fakecmd: switch (kbd_state) { case kbd_cmd: switch (ch) { case 'q': case 3: kbd_state = kbd_exit; if ((stat & (stat_eof | stat_susp))) { clear_cur_line(); 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; } 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_exit: break; } if ((stat & stat_dirty)) { redraw(circbuf, nlines, hpos, columns, stat); stat &= ~stat_dirty; } } } } if (tcsetattr(ttyfd, TCSANOW, &tty_saved) < 0) panic("unable to restore TTY parameters"); return exit_status; }