/*
 *	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 <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <curses.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>


#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);
}



syntax highlighted by Code2HTML, v. 0.9.1