/* watch -- execute a program repeatedly, displaying output fullscreen
 *
 * Based on the original 1991 'watch' by Tony Rems <rembo@unisoft.com>
 * (with mods and corrections by Francois Pinard).
 *
 * Substantially reworked, new features (differences option, SIGWINCH
 * handling, unlimited command length, long line handling) added Apr 1999 by
 * Mike Coleman <mkc@acm.org>.
 */


#define VERSION "0.2.0"

#include <ctype.h>
#include <getopt.h>
#include <signal.h>
#include <ncurses.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <time.h>
#include <unistd.h>
#include <locale.h>

static struct option longopts[] =
  {
    { "differences", optional_argument, 0, 'd' },
    { "help", no_argument, 0, 'h' },
    { "interval", required_argument, 0, 'n' },
    { "version", no_argument, 0, 'v' },
    { 0, 0, 0, 0 }
  };

static char usage[] = "Usage: %s [-dhnv] [--differences[=cumulative]] [--help] [--interval=<n>] [--version] <command>\n";


static char *progname;

static int curses_started = 0;
static int height=24, width=80;
static int screen_size_changed=0;
static int first_screen=1;


#define min(x,y) ((x) > (y) ? (y) : (x))
#define max(x,y) ((x) > (y) ? (x) : (y))


static void
do_usage(void)
{
  fprintf(stderr, usage, progname);
  exit(1);
}


static void
do_exit(int status) {
  if (curses_started)
    endwin();
  exit(status);
}


/* signal handler */
static void
die(int notused)
{
  (void)notused;
  do_exit(0);
}


static void
winch_handler(int notused)
{
  (void)notused;
  screen_size_changed = 1;
}


static void
get_terminal_size(void)
{
  struct winsize w;
  if (ioctl(2, TIOCGWINSZ, &w) == 0)
    {
      if (w.ws_row > 0)
	height = w.ws_row;
      if (w.ws_col > 0)
	width = w.ws_col;
    }
}


int
main(int argc, char *argv[])
{
  int optc;
  int option_differences=0,
    option_differences_cumulative=0,
    option_help=0,
    option_version=0;
  int interval=2;
  char *command;
  int command_length=0;		/* not including final \0 */
  int s;
  char *endp;

  setlocale(LC_ALL,"");
  progname = argv[0];

  while ((optc = getopt_long(argc, argv, "+d::hn:v", longopts, (int *) 0))
	 != EOF)
    {
      switch (optc)
	{
	case 'd':
	  option_differences = 1;
	  if (optarg)
	    option_differences_cumulative = 1;
	  break;
	case 'h':
	  option_help = 1;
	  break;
	case 'n':
	  {
	    char *str;
	    interval = strtol(optarg, &str, 10);
	    if (!*optarg || *str)
	      do_usage();
	  }
	  break;
	case 'v':
	  option_version = 1;
	  break;
	default:
	  do_usage();
	  break;
	}
    }

  if (option_version)
    {
      fprintf (stderr, "%s\n", VERSION);
      if (!option_help)
	exit(0);
    }

  if (option_help)
    {
      fprintf(stderr, usage, progname);
      fputs("  -d, --differences[=cumulative]\thighlight changes between updates\n", stderr);
      fputs("\t\t(cumulative means highlighting is cumulative)\n", stderr);
      fputs("  -h, --help\t\t\t\tprint a summary of the options\n", stderr);
      fputs("  -n, --interval=<seconds>\t\tseconds to wait between updates\n", stderr);
      fputs("  -v, --version\t\t\t\tprint the version number\n", stderr);
      exit(0);
    }

  if (optind >= argc)
    do_usage();

  command_length = strlen(argv[optind]);
  command = (char*)malloc(command_length + 1); /* space or \0 */
  memcpy(command, argv[optind++], command_length);
  command[command_length] = '\0';
  for (;optind<argc;optind++)
    {
      s = strlen(argv[optind]);
      command = realloc(command, command_length+s+2); /* space and \0 */
      endp = command + command_length;
      *endp = ' ';
      memcpy(endp+1, argv[optind],s);
      command_length += 1+s; /* space then string length */
      command[command_length] = '\0';
    }

  get_terminal_size();

  /* Catch keyboard interrupts so we can put tty back in a sane state.  */
  signal(SIGINT, die);
  signal(SIGTERM, die);
  signal(SIGHUP, die);
  signal(SIGWINCH, winch_handler);

  /* Set up tty for curses use.  */
  curses_started = 1;
  initscr();
  nonl();
  noecho();
  cbreak();

  for(;;)
    {
      time_t t = time(NULL);
      char *ts = ctime(&t);
      int tsl = strlen(ts);
      char *header;
      FILE *p;
      int x, y;

      if (screen_size_changed)
	{
	  get_terminal_size();
	  resizeterm(height, width);
	  clear();
	  /* redrawwin(stdscr); */
	  screen_size_changed = 0;
	  first_screen = 1;
	}

      /* left justify interval and command, right justify time, clipping all
	 to fit window width */
      asprintf(&header, "Every %ds: %.*s",
	       interval, min(width-1, command_length), command);
      mvaddstr(0, 0, header);
      if (strlen(header) > (size_t)(width - tsl - 1))
        mvaddstr(0, width - tsl - 4, "...  ");
      mvaddstr(0, width - tsl + 1, ts);
      free(header);

      if (!(p = popen(command, "r")))
	{
	  perror("popen");
	  do_exit(2);
	}

      for (y=2; y<height; y++)
	{
	  int eolseen = 0, tabpending = 0;
	  for (x=0; x<width; x++)
	    {
	      int c = ' ';
	      int attr = 0;
	      
	      if (!eolseen)
		{
		  /* if there is a tab pending, just spit spaces until the
		     next stop instead of reading characters */
		  if (!tabpending)
		    do
		      c = getc(p);
		    while (c != EOF && !isprint(c) && c != '\n' && c != '\t');
		  if (c == '\n')
		    if (x == 0) {
		      x=-1;
		      continue;
		    } else
		    eolseen = 1;
		  else if (c == '\t')
		    tabpending = 1;
		  if (c == EOF || c == '\n' || c == '\t')
		    c = ' ';
		  if (tabpending && (((x + 1) % 8) == 0))
		    tabpending = 0;
		}
	      move(y, x);
	      if (option_differences)
		{
		  int oldch = inch();
		  char oldc = oldch & A_CHARTEXT;
		  attr = !first_screen
		    && (c != oldc
			|| (option_differences_cumulative
			    && (oldch & A_ATTRIBUTES)));
		}
	      if (attr)
		standout();
	      addch(c);
	      if (attr)
		standend();
	    }
	}

      pclose(p);

      first_screen = 0;
      refresh();
      sleep(interval);
    }

  endwin();

  return 0;
}