/* * wtail - watch multiple files * AYM 2002-03-23 */ /* This file is copyright André Majorel 2002-2003. This program is free software; you can redistribute it and/or modify it under the terms of version 2 of the GNU General Public License as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. */ #include #include #include #include #include #include #include #include #include #include #include #define IBUFSZ 16384 /* Read up to that many chars at a time */ #define OBUFSZ 512 /* Maximum number of characters that it's safe to print with curses. The exact value is unknown. A quick search in SUS v2, the ncurses man pages and n869 turned up nothing. What I do know is that a value of 4096 can make ncurses drop characters. */ typedef struct { const char *pathname; int errno_; int fd; unsigned long line; int datay; int datah; WINDOW *dataw; int statusy; WINDOW *statusw; } file_t; /* Assuming ISO 8859-* display */ #define MYISCTRL(c) ((((c) & 0x60) == 0 && (c) != 0x09 && (c) != 0x0a)\ || (c) == 0x7f) extern const char version[]; static void err (const char *fmt, ...); static int wputsm (WINDOW *win, int height, const char *str); int main (int argc, char *argv[]) { int exit_status = 0; size_t nfiles; file_t *files = NULL; float window_lines; unsigned char *buf = NULL; /* Parse the command line */ { int g; if (argc == 2 && strcmp (argv[1], "--help") == 0) { puts ("Usage:\n" " wtail --help\n" " wtail --version\n" " wtail file ..."); puts ("Options:\n" " --help Print usage to standard output and exit successfully\n" " --version Print version number to standard output and exit successfully"); exit (0); } if (argc == 2 && strcmp (argv[1], "--version") == 0) { puts (version); exit (0); } while ((g = getopt (argc, argv, "")) != EOF) { exit (1); } } if (optind >= argc) { err ("not enough arguments"); exit (1); } nfiles = argc - optind; files = malloc (nfiles * sizeof *files); if (files == NULL) { err ("%s", strerror (ENOMEM)); exit (1); } { size_t f; for (f = 0; f < nfiles; f++) { files[f].pathname = argv[optind + f]; files[f].fd = -1; files[f].errno_ = 0; files[f].line = 0; } } buf = malloc (IBUFSZ + 1); if (buf == NULL) { err ("%s", strerror (ENOMEM)); exit (1); } initscr (); cbreak (); intrflush (stdscr, FALSE); leaveok (stdscr, TRUE); noecho (); nonl (); refresh (); /* Create a window for each file */ if (LINES < 2 * nfiles) { err ("not enough lines or too many files"); /* FIXME goes to /dev/null */ exit_status = 1; goto byebye; } window_lines = (float) LINES / nfiles; { size_t n; int lines_so_far = 0; for (n = 0; n < nfiles; n++) { int nlines; if (n + 1 == nfiles) nlines = LINES - lines_so_far; else { if (lines_so_far < n * window_lines) nlines = (int) window_lines + 1; else nlines = (int) window_lines; } if (nlines < 2) { err ("internal error"); exit (1); } files[n].datay = lines_so_far; files[n].statusy = lines_so_far + nlines - 1; files[n].datah = nlines - 1; files[n].dataw = subwin (stdscr, files[n].datah, COLS, files[n].datay, 0); files[n].statusw = subwin (stdscr, 1, COLS, files[n].statusy, 0); idlok (files[n].dataw, TRUE); scrollok (files[n].dataw, TRUE); wattron (files[n].statusw, A_REVERSE); /* Initialise status bar */ mvwprintw (files[n].statusw, 0, 0, "%-*.*s", (size_t) COLS, (size_t) COLS, files[n].pathname); wnoutrefresh (files[n].statusw); lines_so_far += nlines; } } doupdate (); nodelay (stdscr, TRUE); /* Main loop : waiting for data on any of the files. Wake up every 100 ms second to check the keyboard. */ for (;;) { size_t n; int k; fd_set readfds; int fdmax = -1; struct timeval timeout; static int ateof = 0; /* Poll the keyboard */ k = getch (); if (k == 'q' || k == 'Q' || k == '\x11' || k == '\x1b') break; /* If the last time we tried to read, we got EOF on all files, wait for a while before polling them again. */ if (ateof) { struct timeval nap; nap.tv_sec = 0; nap.tv_usec = 100000; select (0, NULL, NULL, NULL, &nap); } /* Keep trying to open files */ { size_t f; for (f = 0; f < nfiles; f++) { if (files[f].fd < 0) { files[f].fd = open (files[f].pathname, O_RDONLY); if (files[f].fd < 0) { /* Print the cause of the failure in the middle of the data window. */ if (files[f].errno_ != errno) { wputsm (files[f].dataw, files[f].datah, strerror (errno)); wnoutrefresh (files[f].dataw); files[f].errno_ = errno; } continue; } files[f].line = 0; wclear (files[f].dataw); wnoutrefresh (files[f].dataw); } } } /* Poll the files */ ateof = 1; FD_ZERO (&readfds); { size_t f; for (f = 0; f < nfiles; f++) { if (files[f].fd < 0) continue; FD_SET (files[f].fd, &readfds); if (files[f].fd > fdmax) fdmax = files[f].fd; } } timeout.tv_sec = 0; timeout.tv_usec = 0; if (select (fdmax + 1, &readfds, NULL, NULL, &timeout) < 1) continue; for (n = 0; n < nfiles; n++) { if (files[n].fd < 0) continue; if (FD_ISSET (files[n].fd, &readfds)) { unsigned long line = files[n].line; ssize_t nbytes; if ((nbytes = read (files[n].fd, buf, IBUFSZ)) > 0) { unsigned char *p = buf; unsigned char *const pmax = buf + nbytes; ateof = 0; if (files[n].line == 0) files[n].line = 1; for (;;) { /* Print a run of printable characters */ { unsigned char *p0 = p; while (p < pmax && ! MYISCTRL (*p)) { if (*p == '\n') files[n].line++; p++; if (p >= p0 + OBUFSZ) { wprintw (files[n].dataw, "%.*s", (size_t) (p - p0), p0); p0 = p; } } if (p > p0) wprintw (files[n].dataw, "%.*s", (size_t) (p - p0), p0); #if 0 wattron (files[n].dataw, A_STANDOUT); waddch (files[n].dataw, '#'); wattroff (files[n].dataw, A_STANDOUT); #endif if (p >= pmax) break; } /* Print a run of control characters */ wattron (files[n].dataw, A_REVERSE); while (p < pmax && MYISCTRL (*p)) { if (*p >= 0x80) { waddch (files[n].dataw, '~'); waddch (files[n].dataw, *p - 0x40); } else if (*p == 0x7f) { waddstr (files[n].dataw, "^?"); } else { waddch (files[n].dataw, '^'); waddch (files[n].dataw, *p + 0x40); } p++; } wattroff (files[n].dataw, A_REVERSE); if (p >= pmax) break; } wnoutrefresh (files[n].dataw); } /* If the line number has changed, refresh */ if (files[n].line != line) { int x; line = files[n].line; for (x = COLS - 1; x >= 0; x--) { mvwaddch (files[n].statusw, 0, x, '0' + line % 10); line /= 10; if (line == 0) break; } wnoutrefresh (files[n].statusw); } } } doupdate (); } byebye: /* FIXME close files[*].fd */ /* FIXME free files */ endwin (); if (buf != NULL) free (buf); exit (exit_status); } static void err (const char *fmt, ...) { va_list argp; fputs ("wtail: ", stderr); va_start (argp, fmt); vfprintf (stderr, fmt, argp); va_end (argp); fputc ('\n', stderr); } /* * wputsm - write a string in the middle of a window */ static int wputsm (WINDOW *win, int height, const char *str) { size_t len = strlen (str); int xpos = (COLS - len) / 2; int strofs = 0; if (xpos < 0) { strofs += xpos; xpos = 0; } return mvwprintw (win, height / 2, xpos, "%.*s", (size_t) COLS, str + strofs); }