/*
 * conspy
 * ------
 *
 * A text-mode VNC like program for Linux virtual terminals.
 *
 * Author: Russell Stuart, russell-conspy@stuart.id.au
 *         22/05/2003
 *
 * To compile:
 *   gcc -Werror -Wall -Wextra -O2 --std=c99 -lncurses -o conspy conspy.c
 *
 *
 * License
 * -------
 *
 * Copyright (c) 2005,2006,2007,2008,2009,2011 Russell Stuart.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
#define	_GNU_SOURCE
#include <curses.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <signal.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/kd.h>
#include <sys/select.h>
#include <sys/stat.h>
#include <termios.h>
#include <term.h>
#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>

/* GNU/kFreeBSD has different #define's */
#if defined(__FreeBSD_kernel__)
#define K_UNICODE K_CODE
#define IUCLC 0
#endif

extern int errno;

/*
 * Version info.
 */
static char conspy_date[]	= "2011-04-17";
static char conspy_version[]	= "1.8";

/*
 * VGA colour definitions, as found in a nibble in an attribute
 * byte within VGA video memory.
 */
#define	VGA_BLACK	0x00
#define	VGA_BLUE	0x01
#define	VGA_GREEN	0x02
#define	VGA_CYAN	0x03
#define	VGA_RED		0x04
#define	VGA_MAGENTA	0x05
#define	VGA_YELLOW	0x06
#define	VGA_WHITE	0x07

/*
 * Box characters used by the Linux console.
 */
#define	IBM_BLOCK	0x0c
#define	IBM_BOARD	0x09
#define	IBM_BTEE	0xcb
#define	IBM_CKBOARD	0x0a
#define	IBM_DARROW	0x19
#define	IBM_DEGREE	0xb0
#define	IBM_GEQUAL	0x14
#define	IBM_HLINE	0xca
#define	IBM_LANTERN	0xdf
#define	IBM_LARROW	0x16
#define	IBM_LLCORNER	0xc3
#define	IBM_LRCORNER	0xc9
#define	IBM_LTEE	0xc7
#define	IBM_PI		0x1f
#define	IBM_PLMINUS	0xb1
#define	IBM_RTEE	0xcd
#define	IBM_STERLING	0xa3
#define	IBM_TTEE	0xce
#define	IBM_UARROW	0x18
#define	IBM_ULCORNER	0xc6
#define	IBM_URCORNER	0xcc
#define	IBM_VLINE	0xc5

/*
 * This is the original IBM PC charcter set.  I thought this is what the
 * Linux console would use, but apparently not.
 */
#if	0
#define	IBM_BLOCK	0xdb
#define	IBM_BTEE	0xc1
#define	IBM_BULLET	0xf9
#define	IBM_DARROW	0x19
#define	IBM_D_BTEE	0xca
#define	IBM_DEGREE	0xf8
#define	IBM_D_HLINE	0xcd
#define	IBM_DIAMOND	0x04
#define	IBM_D_LLCORNER	0xc8
#define	IBM_D_LRCORNER	0xbc
#define	IBM_D_LTEE	0xcc
#define	IBM_D_RTEE	0xb9
#define	IBM_DS_BTEE	0xcf
#define	IBM_DS_LLCORNER	0xd3
#define	IBM_DS_LRCORNER	0xbd
#define	IBM_DS_LTEE	0xc7
#define	IBM_DS_RTEE	0xb6
#define	IBM_DS_TTEE	0xd1
#define	IBM_DS_ULCORNER	0xd6
#define	IBM_DS_URCORNER	0xb7
#define	IBM_DS_XCROSS	0xd7
#define	IBM_D_TTEE	0xcb
#define	IBM_D_ULCORNER	0xc9
#define	IBM_D_URCORNER	0xbb
#define	IBM_D_VLINE	0xba
#define	IBM_D_XCROSS	0xce
#define	IBM_GEQUAL	0xf2
#define	IBM_HLINE	0xc4
#define	IBM_LANTERN	0x0f
#define	IBM_LARROW	0x1b
#define	IBM_LLCORNER	0xc0
#define	IBM_LRCORNER	0xd9
#define	IBM_LTEE	0xc3
#define	IBM_PLMINUS	0xf1
#define	IBM_RARROW	0x1a
#define	IBM_RTEE	0xb4
#define	IBM_SD_BTEE	0xd0
#define	IBM_SD_LLCORNER	0xd4
#define	IBM_SD_LRCORNER	0xbe
#define	IBM_SD_LTEE	0xc6
#define	IBM_SD_RTEE	0xb5
#define	IBM_SD_TTEE	0xd2
#define	IBM_SD_ULCORNER	0xd5
#define	IBM_SD_URCORNER	0xb8
#define	IBM_SD_XCROSS	0xd8
#define	IBM_STERLING	0x9c
#define	IBM_TTEE	0xc2
#define	IBM_UARROW	0x18
#define	IBM_ULCORNER	0xda
#define	IBM_URCORNER	0xbf
#define	IBM_VLINE	0xb3
#define	IBM_XCROSS	0xc5
#endif

/*
 * This function maps a VGA colour pair to a curses COLOR_PAIR()
 * number.  In the curses scheme colour pair 0 must be white text
 * on a black background, so the origin is moved to there.
 */
#define	VGA_PAIR(foreground, background) \
			(((foreground) + ((background) << 3)) ^ 0x07)

/*
 * Forward declarations.
 */
static void cleanup();
static void conspy(int use_colour);
static void finish(int signal);
static void init_cursesbox();
static void process_command_line(int argc, char** argv);
static int setup();
static void syserror(char* message, ...);
static void usage(char* message, ...);

/*
 * Local variables.
 */
static char*		me;
static struct termios	old_termios;
static int		opt_viewonly;
static int  		tty_handle = -1;
static char 		tty_name[20];
static int  		device_handle = -1;
static char 		device_name[20];

/*
 * This array translates a VGA colour (defined above) to a
 * CURSES colour.
 */
static short		colour_map[] =
  { COLOR_BLACK,	/* VGA_BLACK */
    COLOR_BLUE,		/* VGA_BLUE */
    COLOR_GREEN,	/* VGA_GREEN */
    COLOR_CYAN,		/* VGA_CYAN */
    COLOR_RED,		/* VGA_RED */
    COLOR_MAGENTA,	/* VGA_MAGENTA */
    COLOR_YELLOW,	/* VGA_YELLOW */
    COLOR_WHITE,	/* VGA_WHITE */
  };

/*
 * Special IBM characters & their translations.
 */
static unsigned short cursesbox[256];

/*
 * A character as it appears in the VGA video buffer.
 */
struct vidchar {
#if 0
  unsigned char		vidchar_char;		/* The IBM-ASCII Char code */
  unsigned char		vidchar_attribute;	/* Colour/blink/bold spec */
#define	VIDCHAR_CHAR(vidchar)		((vidchar)->vidchar_char)
#define	VIDCHAR_ATTRIBUTE(vidchar)	((vidchar)->vidchar_attribute)
#else
  unsigned short	vidchar_charattr;	/* Attr in msb, char in lsb */
#define	VIDCHAR_CHAR(vidchar)		((vidchar)->vidchar_charattr & 0xFF)
#define	VIDCHAR_ATTRIBUTE(vidchar)	((vidchar)->vidchar_charattr >> 8)
#endif
};


/*
 * The data returned by reading a /dev/vcsa device.
 */
struct vidbuf {
  unsigned char		vidbuf_lines;		/* Line on screen */
  unsigned char		vidbuf_columns;		/* Columns on screen */
  unsigned char		vidbuf_curcolumn;	/* Column cursor is in */
  unsigned char		vidbuf_curline;		/* Line cursor is in */
  struct vidchar	vidbuf_chars[200*80];	/* Char in VGA video buf */
};

/*
 * Options we allow.
 */
static struct option options[] =
  {
    {"version",		0,	0,	'V'},
    {"viewonly",	0,	0,	'v'},
    {0,0,0,0},
  };

/*
 * Entry point.
 */
int main(int argc, char** argv)
{
  int			use_colour;

  me = strrchr(argv[0], '/');
  me = me == 0 ? argv[0] : me + 1;
  process_command_line(argc, argv);
  use_colour = setup();
  init_cursesbox();
  conspy(use_colour);
  cleanup();
  /*
   * Clear screen & home cursor.  Added when _I_ became confused
   * as to whether this program was running or not(!)
   */
  if (tigetstr("clear") != (char*)0)
  {
    putp(tigetstr("clear"));
    fflush(stdout);
  }
  return 0;
}

/*
 * Print our a usage message and exit.
 */
static void usage(char* message, ...)
{
  va_list		list;

  if (message != 0)
  {
    fprintf(stderr, "%s: ", me);
    va_start(list, message);
    vfprintf(stderr, message, list);
    va_end(list);
    fprintf(stderr, ".\n");
  }
  fprintf(stderr, "usage: %s [options] [virtual_console].\n", me);
  fprintf(stderr, "options:\n");
  fprintf(stderr, "  -V,--version   Print %s's version number and exit.\n", me);
  fprintf(stderr, "  -v,--viewonly  Don't send keystrokes to the console.\n");
  fprintf(stderr, "virtual_console:\n");
  fprintf(stderr, "  omitted  Track the current console.\n");
  fprintf(stderr, "  1..63    Virtual console N.\n");
  fprintf(stderr, "To exit, quickly press escape 3 times.\n");
  exit(1);
}

/*
 * Process the command line.
 */
static void process_command_line(int argc, char** argv)
{
  char*			end;
  int			opt;
  size_t		optindex;
  char			opts[(sizeof(options) / sizeof(*options))*2 + 1];
  char 			vcc_name[sizeof(device_name)];
  int			vcc_errno;
  char 			vcsa_name[sizeof(device_name)];
  int			vcsa_errno;
  char*			virtual_console;

  opt = 0;
  for (optindex = 0; optindex < sizeof(options)/sizeof(*options); optindex += 1)
  {
    opts[opt++] = options[optindex].val;
    if (options[optindex].has_arg)
      opts[opt++] = ':';
  }
  opts[opt] = '\0';
  while ((opt = getopt_long(argc, argv, opts, options, 0)) != -1)
  {
    switch (opt)
    {
      case 'V':
	printf("%s: version %s %s\n", me, conspy_version, conspy_date);
	exit(0);
      case 'v':
	opt_viewonly = 1;
	break;
      default:
	usage(0);
    }
  }
  virtual_console = argv[optind];
  if (virtual_console != 0)
  {
    strtol(virtual_console, &end, 10);
    if (*end != '\0' || end - virtual_console > 2)
      usage("invalid virtual console \"%s\"", virtual_console);
  }
  /*
   *  Verify we can open the devices.
   */
  strcpy(vcsa_name, "/dev/vcsa");
  vcc_errno = 0;
  if (virtual_console != 0)
    strcat(vcsa_name, virtual_console);
  strcpy(vcc_name, "/dev/vcc/a");
  if (virtual_console != 0 && *virtual_console != '\0')
    strcat(vcc_name, virtual_console);
  else
    strcat(vcc_name, "0");
  strcpy(device_name, vcsa_name);
  device_handle = open(vcsa_name, O_RDONLY);
  vcsa_errno = errno;
  if (device_handle == -1)
  {
    strcpy(device_name, vcc_name);
    device_handle = open(vcc_name, O_RDONLY);
    vcc_errno = errno;
  }
  if (device_handle == -1)
  {
    fprintf(stderr, "%s: could not open either the alternate device files.\n", me);
    errno = vcsa_errno;
    perror(vcsa_name);
    errno = vcc_errno;
    perror(vcc_name);
    exit(1);
  }
  if (!opt_viewonly)
  {
    strcpy(tty_name, "/dev/tty");
    if (virtual_console != 0 && *virtual_console != '\0')
      strcat(tty_name, virtual_console);
    else
      strcat(tty_name, "0");
    tty_handle = open(tty_name, O_WRONLY);
    if (tty_handle == -1 && errno == ENOENT)
    {
      strcpy(tty_name, "/dev/vc/");
      if (virtual_console != 0 && *virtual_console != '\0')
	strcat(tty_name, virtual_console);
      else
	strcat(tty_name, "0");
      tty_handle = open(tty_name, O_WRONLY);
    }
    if (tty_handle == -1)
    {
      perror(tty_name);
      exit(1);
    }
  }
}

/*
 * Initialise the Curses box char set.  This must follow
 */
static void init_cursesbox()
{
  cursesbox[IBM_BLOCK]		= ACS_BLOCK;
  cursesbox[IBM_BOARD]		= ACS_BOARD;
  cursesbox[IBM_BTEE]		= ACS_BTEE;
  cursesbox[IBM_CKBOARD]	= ACS_CKBOARD;
  cursesbox[IBM_DARROW]		= ACS_DARROW;
  cursesbox[IBM_DEGREE]		= ACS_DEGREE;
  cursesbox[IBM_GEQUAL]		= ACS_GEQUAL;
  cursesbox[IBM_HLINE]		= ACS_HLINE;
  cursesbox[IBM_LANTERN]	= ACS_LANTERN;
  cursesbox[IBM_LARROW]		= ACS_LARROW;
  cursesbox[IBM_LLCORNER]	= ACS_LLCORNER;
  cursesbox[IBM_LRCORNER]	= ACS_LRCORNER;
  cursesbox[IBM_LTEE]		= ACS_LTEE;
  cursesbox[IBM_PI]		= ACS_PI;
  cursesbox[IBM_PLMINUS]	= ACS_PLMINUS;
  cursesbox[IBM_RTEE]		= ACS_RTEE;
  cursesbox[IBM_STERLING]	= ACS_STERLING;
  cursesbox[IBM_TTEE]		= ACS_TTEE;
  cursesbox[IBM_UARROW]		= ACS_UARROW;
  cursesbox[IBM_ULCORNER]	= ACS_ULCORNER;
  cursesbox[IBM_URCORNER]	= ACS_URCORNER;
  cursesbox[IBM_VLINE]		= ACS_VLINE;
}

/*
 * Print an OS error and die.
 */
static void syserror(char* message, ...)
{
  int			errnr = errno;
  va_list		list;

  cleanup();
  va_start(list, message);
  vfprintf(stderr, message, list);
  va_end(list);
  fprintf(stderr, ": %s.\n", strerror(errnr));
  exit(1);
}

/*
 * Die, possibly from a signal.
 */
static void finish(int sig)
{
  sigset_t              sigset;

  cleanup();
  if (sig <= 0)
    exit(-sig);
  sigemptyset(&sigset);
  sigaddset(&sigset, sig);
  sigprocmask(SIG_UNBLOCK, &sigset, 0);
  signal(sig, SIG_DFL);
  kill(getpid(), sig);
  pause();
}

/*
 * Set up curses and the TTY.
 */
static int setup()
{
  int			colour;
  int			background;
  int			foreground;
  struct termios	termios;
  int			use_colour;

  /*
   * Get a copy of the current TTY settings.
   */
  if (tcgetattr(0, &old_termios) == -1)
  {
    perror("tcgetattr(0)");
    exit(1);
  }
  /*
   * Start curses.
   */
  (void)signal(SIGHUP, finish);
  (void)signal(SIGINT, finish);
  (void)signal(SIGTERM, finish);
  (void)initscr();
  (void)nonl();
  /*
   * Set up the tty.  All characters must be passed through to
   * us unaltered.
   */
  termios = old_termios;
  termios.c_iflag &= ~(BRKINT|INLCR|ICRNL|IXON|IXOFF|IUCLC|IXANY|IMAXBEL);
  termios.c_oflag &= ~(OPOST);
  termios.c_lflag &= ~(ISIG|ICANON|ECHO);
  if (tcsetattr(0, TCSANOW, &termios) == -1)
    syserror("tcsetattr(0)");
  /*
   * Set up the colour map, if we can.
   */
  use_colour = 0;
  if (has_colors())
  {
    start_color();
    if (COLOR_PAIRS >= 64)
      use_colour = 1;
  }
  if (use_colour)
  {
    for (foreground = 0; foreground < 8; foreground += 1)
    {
      for (background = 0; background < 8; background += 1)
      {
	colour = VGA_PAIR(foreground, background);
	if (colour != 0)
	  init_pair(colour, colour_map[foreground], colour_map[background]);
      }
    }
  }
  return use_colour;
}

/*
 * Shut down curses, and restore everything.
 */
static void cleanup()
{
  tcsetattr(0, TCSANOW, &old_termios);
  endwin();
  if (device_handle != -1)
    close(device_handle);
  if (tty_handle != -1)
    close(tty_handle);
}

/*
 * This is where the actual work is done.
 */
static void conspy(int use_colour)
{
  unsigned short	box;
  int			buffer_size;
  int			bytes_read;
  unsigned int		column;
  attr_t		curses_attribute;
  short			curses_colour;
  int			escape_pressed;
  int			escape_notpressed;
  int			ioerror_count;
  int			key_count;
  int			key_index;
  long			keyboard_mode;
  char			keys_pressed[256];
  unsigned int		last_attribute;
  unsigned int		last_columns;
  unsigned int		last_rows;
  unsigned int		line;
  chtype		line_buf[256];
  int			line_chars;
  fd_set		readset;
  int			result;
  struct timeval	timeval;
  int			tty_result;
  unsigned int		video_attribute;
  int			video_char;
  struct vidbuf		vidbuf;
  struct vidchar*	vidchar;

  curses_colour = 0;
  curses_attribute = 0;
  escape_notpressed = 0;
  escape_pressed = 0;
  ioerror_count = 0;
  key_count = 0;
  last_attribute = ~0U;
  last_columns = 0;
  last_rows = 0;
  for (;;)
  {
    /*
     * Read the video buffer.
     */
    if (lseek(device_handle, 0L, SEEK_SET) != 0L)
      syserror(device_name);
    bytes_read = read(device_handle, &vidbuf, sizeof(vidbuf));
    if (bytes_read == -1)
      syserror(device_name);
    buffer_size = 4 +
	vidbuf.vidbuf_lines * vidbuf.vidbuf_columns * sizeof(struct vidchar);
    if (buffer_size > (int)sizeof(vidbuf))
    {
      fprintf(
	  stderr, "%s: screen too large.  I only handle up to 200x80.\n", me);
      cleanup();
      exit(1);
    }
    if (bytes_read != buffer_size)
      syserror("read wrong number of chars");
    /*
     * If the screen size has changed blank out the unused portions.
     */
    if (vidbuf.vidbuf_lines < last_rows && last_rows < (unsigned)LINES)
    {
      move(vidbuf.vidbuf_lines, 0);
      clrtobot();
    }
    if (vidbuf.vidbuf_columns < last_columns && last_columns < (unsigned)COLS)
    {
      for (line = 0; line < last_rows && line < (unsigned)LINES; line += 1)
      {
	move(line, last_columns);
	clrtoeol();
      }
    }
    last_rows = vidbuf.vidbuf_lines;
    last_columns = vidbuf.vidbuf_columns;
    /*
     * Write the data to the screen.
     */
    vidchar = vidbuf.vidbuf_chars;
    for (line = 0; line < vidbuf.vidbuf_lines && line < (unsigned)LINES; line += 1)
    {
      line_chars = 0;
      for (column = 0; column < vidbuf.vidbuf_columns; column += 1)
      {
	if (column >= (unsigned)COLS)
	{
	  vidchar += vidbuf.vidbuf_columns - column;
	  break;
	}
	video_attribute = VIDCHAR_ATTRIBUTE(vidchar);
	video_char = VIDCHAR_CHAR(vidchar);
	box = cursesbox[video_char];
	if (box != 0)
	{
	  video_attribute |= 0x100;
	  video_char = box;
	}
	if (video_char < ' ')
	  video_char = ' ';
	if (video_attribute != last_attribute)
	{
	  if (line_chars > 0)
	  {
	    move(line, column - line_chars);
	    addchnstr(line_buf, line_chars);
	    wchgat(stdscr, line_chars, curses_attribute, curses_colour, 0);
	    line_chars = 0;
	  }
	  curses_attribute = A_NORMAL;
	  if (video_attribute & 0x100)
	    curses_attribute |= A_ALTCHARSET;
	  if (video_attribute & 0x80)
	    curses_attribute |= A_BLINK;
	  if (video_attribute & 0x08)
	    curses_attribute |= A_BOLD;
	  if (use_colour)
	  {
	    curses_colour =
		VGA_PAIR(video_attribute & 0x7, video_attribute>>4 & 0x7);
	  }
	  last_attribute = video_attribute;
	}
	line_buf[line_chars++] = video_char;
	vidchar += 1;
      }
      move(line, column - line_chars);
      addchnstr(line_buf, line_chars);
      wchgat(stdscr, line_chars, curses_attribute, curses_colour, 0);
    }
    if (vidbuf.vidbuf_curline < LINES && vidbuf.vidbuf_curcolumn < COLS)
      move(vidbuf.vidbuf_curline, vidbuf.vidbuf_curcolumn);
    refresh();
    /*
     * Wait for 1/4 or a second, or for a character to be pressed.
     */
    FD_ZERO(&readset);
    FD_SET(0, &readset);
    timeval.tv_sec = 0;
    timeval.tv_usec = 250 * 1000L;
    result = select(0 + 1, &readset, 0, 0, &timeval);
    if (result == -1)
    {
      if (errno != EINTR)
	syserror("select([tty_handle],0,0,timeval)");
      endwin();
      refresh();
      continue;
    }
    /*
     * Read the keys pressed.
     */
    bytes_read = 0;
    if (result == 1)
    {
      bytes_read =
	read(0, keys_pressed + key_count, sizeof(keys_pressed) - key_count);
      if (bytes_read < 0)
	syserror(tty_name);
    }
    /*
     * Do exit processing.
     */
    if (result == 0 && ++escape_notpressed == 4)
    {					/* >1sec since last key press */
      escape_pressed = 0;		/* That ends any exit sequence */
      escape_notpressed = 0;
    }
    for (key_index = key_count; key_index<key_count+bytes_read; key_index += 1)
    {					/* See if escape pressed 3 times */
      if (keys_pressed[key_index] != '\033')
	escape_pressed = 0;
      else if (++escape_pressed == 3)
	return;
      if (keys_pressed[key_index] == ('L' & 0x1F))
	wrefresh(curscr);
    }
    /*
     * Insert all keys pressed into the virtual console's input
     * buffer.  Don't do this if the virtual console is in scan
     * code mode - giving ASCII characters to a program expecting
     * scan codes will confuse it.
     */
    if (!opt_viewonly)
    {
      /*
       * Close & re-open tty in case they have swapped virtual consoles.
       */
      close(tty_handle);
      tty_handle = open(tty_name, O_WRONLY);
      if (tty_handle == -1)
	syserror(tty_name);
      key_count += bytes_read;
      tty_result = ioctl(tty_handle, KDGKBMODE, &keyboard_mode);
      if (tty_result == -1)
	;
      else if (keyboard_mode != K_XLATE && keyboard_mode != K_UNICODE)
	key_count = 0;			/* Keyboard is in scan code mode */
      else
      {
	for (key_index = 0; key_index < key_count; key_index += 1)
	{
	  tty_result = ioctl(tty_handle, TIOCSTI, keys_pressed + key_index);
	  if (tty_result == -1)
	    break;
	}
	if (key_index == key_count)	/* All keys sent? */
	  key_count = 0;		/* Yes - clear the buffer */
	else
	{
	  memmove(keys_pressed, keys_pressed+key_index, key_count-key_index);
	  key_count -= key_index;
	}
      }
      /*
       * We sometimes get spurious IO errors on the TTY as programs
       * close and re-open it.  Usually they will just go away, if
       * we are patient.
       */
      if (tty_result != -1)
	ioerror_count = 0;
      else if (errno != EIO || ++ioerror_count > 4)
	syserror(tty_name);
    }
  }
}
