aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKaz Kylheku <kaz@kylheku.com>2022-04-26 22:42:43 -0700
committerKaz Kylheku <kaz@kylheku.com>2022-04-26 22:42:43 -0700
commit66ba7fdd8752bc46c9cf14605226f044d259ab05 (patch)
tree8412216d0e33ac15239a9db9befc991741188988
parent9c38bc2efd72beb59ba686a5e323bc7cc4fedcf1 (diff)
downloadpw-66ba7fdd8752bc46c9cf14605226f044d259ab05.tar.gz
pw-66ba7fdd8752bc46c9cf14605226f044d259ab05.tar.bz2
pw-66ba7fdd8752bc46c9cf14605226f044d259ab05.zip
New project, Pipe Watch.
-rw-r--r--Makefile2
-rw-r--r--pw.1248
-rw-r--r--pw.c365
3 files changed, 615 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..b9ff47b
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,2 @@
+CFLAGS ?= -g -O2 -W -Wall -std=c99
+pw:
diff --git a/pw.1 b/pw.1
new file mode 100644
index 0000000..db849fa
--- /dev/null
+++ b/pw.1
@@ -0,0 +1,248 @@
+.\" pw: pipe watch
+.\" Copyright 2022 Kaz Kylheku <kaz@kylheku.com>
+.\"
+.\" BSD-2 License
+.\"
+.\" Redistribution and use in source and binary forms, with or without
+.\" modification, are permitted provided that the following conditions are met:
+.\"
+.\" 1. Redistributions of source code must retain the above copyright notice,
+.\" this list of conditions and the following disclaimer.
+.\"
+.\" 2. Redistributions in binary form must reproduce the above copyright notice,
+.\" this list of conditions and the following disclaimer in the documentation
+.\" and/or other materials provided with the distribution.
+.\"
+.\" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+.\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+.\" ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+.\" LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+.\" CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+.\" SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+.\" INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+.\" CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+.\" ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+.\" POSSIBILITY OF SUCH DAMAGE.
+.TH PW 1 "26 April 2022" "Utility Commands" "Pipe Watch"
+
+.SH NAME
+pw \- Pipe Watch: monitor recent lines of output from pipe
+
+.SH SYNOPSIS
+command | pw [-i interval] [-l interval] [-n number-of-lines] [-d]
+
+.SH DESCRIPTION
+.I pw
+stands for Pipe Watch. This is a utility which continuously reads textual input
+from a pipe or pipe-like source, and maintains a dynamic display of the
+most recently read
+.I N
+lines.
+
+If
+.I pw
+is invoked such that its standard input is a TTY, it simply reads lines
+and prints them in its characteristic way, with control characters replaced by
+caret codes, until end-of-file is encountered. Long lines aren't clipped,
+and there is no interactive mode.
+
+The intended use of
+.I pw
+is that its standard input is a pipe, such as the output of another command,
+or a pipe-like device such as a socket or whatever.
+In this situation,
+.I pw
+expects to be executed in a TTY session in which a
+.B /dev/tty
+device can be opened, for the purposes of obtaining interactive input.
+The remaining description pertains to this interactive mode.
+
+In interactive mode,
+.I pw
+simultaneously monitors its standard input for the arrival of new data,
+as well as the TTY for interactive commands.
+Lines from standard input are placed into a FIFO buffer.
+While the FIFO buffer is not yet full, lines are displayed immediately.
+After the FIFO buffer fills up with the specified number of lines
+(controlled by the
+.B -n
+option) then
+.I pw
+transitions into a mode in which, old lines are bumped from the tail of the
+FIFO as new ones are added to the head, and refresh operations are required in
+order to display the current FIFO contents.
+
+The display only refreshes with the latest FIFO data when
+.IP 1.
+there is some keyboard activity from the terminal; or
+.IP 2.
+when the interval period has expired
+without new input having been seen; or else
+.IP 3.
+whenever the long period elapses.
+.PP
+
+In other words, while the pipe is spewing, and there is
+no keyboard input, the display is updated infrequently, only according to the
+long interval.
+
+The display is also updated when standard input indicates end-of-data.
+In this situation,
+.I pw
+terminates, unless the
+.B -d
+(do not quit) option has been specified. In this situation,
+.I pw
+stays in interactive mode. The the end-of-data status is
+indicated by the string
+.B EOF
+being displayed in the status line after the data.
+
+.SH DISPLAY
+
+Lines displayed by
+.I pw
+are trimmed not to exceed the number of terminal columns. When a line is
+trimmed, it is terminated by the
+.B >
+character to indicate that there are more characters. The display may be
+scrolled interactively to read the long lines.
+
+The ASCII DEL character (127) is displayed as
+.B ^?
+and ASCII control characters are displayed as
+.BR ^@ ,
+.BR ^A ", ..."
+.BR ^Z ", ..."
+.BR ^_ .
+All other characters are sent to the display as-is. No attempt is made
+to account for the width of East Asian Unicode characters, and such.
+
+When the display is scrolled horizontally, the
+.B >
+character appears at the start of each line to indicate this state.
+
+.SH COMMANDS
+
+When
+.I pw
+enters closed loop operation, the following single-key commands are
+available:
+
+.IP "\fBq\fP, \fBCtrl-C\fP"
+Quit the program.
+
+.IP "\fBl\fP, \fILeft Arrow\fP"
+Scroll the display to the left.
+
+.IP "\fBh\fP, \fIRight Arrow\fP"
+Scroll the display to the right.
+
+.IP "\fB0\fP, \fIHome\fP"
+Reset scroll to first column.
+
+.IP \fISpace\fP
+Suspend the entry of lines of new lines of input into the display FIFO.
+In suspended mode, input continues to be read from the pipe, but is discarded.
+The status string
+.B SUSPENDED
+is displayed below the data.
+
+.IP \fIEnter\fP
+Cancel suspended mode, and resume adding data to the FIFO. The
+.B SUSPENDED
+status string disappears. If the status is already
+.B EOF
+then suspend mode cannot be entered.
+
+.SH OPTIONS
+
+.IP "\fB-i\fP \fIreal\fP"
+Set the poll interval to number of seconds specified by
+.IR real .
+This is is a floating-point constant such as 3 or 1.5. Exponential notation
+such as 5E-1 (equivalent to 0.5) is permitted.
+This interval determines the input poll timeout of
+.IR pw 's
+input processing loop. If no data arrives from the primary input, or
+from the TTY for this amount of time, a timeout occurs, and the display is
+refreshed if the FIFO has changed since the last refresh.
+The default poll interval is 1s.
+
+.IP "\fB-l\fP \fIreal\fP"
+Set the long update interval to number of seconds specified by
+.IR real .
+The format is the same as
+.BR -i ,
+and the default value is 10s. Every time the long update interval passes,
+the display is updated, if the FIFO has changed since the last update.
+This happens even when input is arriving too rapidly to permit the poll
+timeout to take place. The purpose of the long interval is to ensure that
+there are updates even when the data source continuously and rapidly produces
+data, and there is no TTY input activity. The long interval should be at
+least several times longer than the short interval. The granularity of the
+timing of the long interval updates depends on the poll interval; in the
+absence of TTY input,
+.I pw
+will not perform any display updates more often that the poll interval,
+even if the long interval is made smaller than the poll interval.
+
+.IP "\fB-n\fP \fIinteger\fP"
+Set the number of lines
+.I N
+to the specified decimal integer value. The default value is 15.
+This value must be positive, and is clipped to the number of display lines
+available in the terminal, less one.
+
+.IP \fB-d\fP
+Disable auto-quit: when no more input is available, instead of updating
+the display one last time and terminating,
+.I pw
+will updating the status to
+.B EOF
+and staying in the interactive mode. This is useful when the last portion
+of the input is of interest, and contains long lines that require
+horizontal scrolling.
+
+.SH TERMINATION STATUS
+
+If
+.I pw
+reaches EOF on standard input without encountering a read error, then its
+termination status will be successful. This is regardless of whether it
+automatically quits, or whether it stays in interactive mode and then quits due
+to a quit command. If the data ends prematurely due to a read error, or if the
+program is asked to quit before all of the data has been read, then
+unsuccessful termination status will be indicated.
+
+Incorrect usage, such as nonexistent options or bad arguments to options,
+result in a diagnostic on standard error and an unsuccessful termination.
+
+Unexpected conditions like out of memory result in abnormal termination (abort).
+
+.SH BUGS
+
+This was written over the course of a couple of hours, and tested only
+interactively.
+
+The program doesn't respond to window size changes. The display format, such
+as the handling of control characters, is hard-coded.
+
+The program uses hard-coded ANSI sequences, so it doesn't support interesting
+old terminals. On the other hand, it carries no dependency on any
+terminal abstraction library/data.
+
+The intervals and number of lines cannot be dynamically adjusted.
+
+Suspend mode is confusing; it doesn't necessarily freeze what is on the
+screen.
+
+There is no support for unwrapping long lines, which would be useful for
+copy and paste.
+
+.SH AUTHOR
+Kaz Kylheku <kaz@kylheku.com>
+
+.SH COPYRIGHT
+Copyright 2022, BSD2 License.
diff --git a/pw.c b/pw.c
new file mode 100644
index 0000000..8d79183
--- /dev/null
+++ b/pw.c
@@ -0,0 +1,365 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include <string.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/poll.h>
+#include <termios.h>
+#include <sys/ioctl.h>
+#include <sys/time.h>
+
+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;
+}